home *** CD-ROM | disk | FTP | other *** search
/ Clickx 47 / Clickx 47.iso / assets / software / Miro_Installer.exe / xulrunner / python / app.py < prev    next >
Encoding:
Python Source  |  2008-01-10  |  94.9 KB  |  2,538 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. import config       # IMPORTANT!! config MUST be imported before downloader
  19. import prefs
  20.  
  21. import database
  22. db = database.defaultDatabase
  23.  
  24. import views
  25. import indexes
  26. import sorts
  27. # import filters
  28. import maps
  29.  
  30. import menu
  31. import util
  32. import feed
  33. import item
  34. import playlist
  35. import tabs
  36.  
  37. import opml
  38. import folder
  39. import autodler
  40. import databaseupgrade
  41. import resources
  42. import selection
  43. import template
  44. import singleclick
  45. import storedatabase
  46. import subscription
  47. import downloader
  48. import autoupdate
  49. import xhtmltools
  50. import guide
  51. import idlenotifier 
  52. import eventloop
  53. import searchengines
  54. import download_utils
  55.  
  56. import os
  57. import re
  58. import shutil
  59. import cgi
  60. import traceback
  61. import threading
  62. import platform
  63. import dialogs
  64. import iconcache
  65. import moviedata
  66. import platformutils
  67. import logging
  68. import theme
  69.  
  70. # These are Python templates for string substitution, not at all
  71. # related to our HTML based templates
  72. from string import Template
  73.  
  74. # Something needs to import this outside of Pyrex. Might as well be app
  75. import templatehelper
  76. import databasehelper
  77. # import fasttypes
  78. import urllib
  79. import menubar # Needed because the XUL port only includes this in pybridge
  80. from gtcache import gettext as _
  81. from gtcache import ngettext
  82. from clock import clock
  83.  
  84. # Global Controller singleton
  85. controller = None
  86.  
  87. # Backend delegate singleton
  88. delegate = None
  89.  
  90. # Run the application. Call this, not start(), on platforms where we
  91. # are responsible for the event loop.
  92. def main():
  93.     platformutils.setupLogging()
  94.     util.setupLogging()
  95.     Controller().Run()
  96.  
  97. # Start up the application and return. Call this, not main(), on
  98. # platform where we are not responsible for the event loop.
  99. def start():
  100.     platformutils.setupLogging()
  101.     util.setupLogging()
  102.     Controller().runNonblocking()
  103.  
  104. def startupFunction(func):
  105.     """Decorator for startup functions.  If they throw an exception, miro will
  106.     show a error dialog and quit.
  107.     """
  108.  
  109.     def wrapped(*args, **kwargs):
  110.         try:
  111.             func(*args, **kwargs)
  112.         except:
  113.             util.failedExn("while finishing starting up")
  114.             frontend.exit(1)
  115.     return wrapped
  116.  
  117. ###############################################################################
  118. #### The Playback Controller base class                                    ####
  119. ###############################################################################
  120.  
  121. class PlaybackControllerBase:
  122.     
  123.     def __init__(self):
  124.         self.currentPlaylist = None
  125.         self.justPlayOne = False
  126.         self.currentItem = None
  127.         self.updateVideoTimeDC = None
  128.  
  129.     def configure(self, view, firstItemId=None, justPlayOne=False):
  130.         self.currentPlaylist = Playlist(view, firstItemId)
  131.         self.justPlayOne = justPlayOne
  132.     
  133.     def reset(self):
  134.         if self.currentPlaylist is not None:
  135.             eventloop.addIdle (self.currentPlaylist.reset, "Reset Playlist")
  136.             self.currentPlaylist = None
  137.  
  138.     def configureWithSelection(self):
  139.         itemSelection = controller.selection.itemListSelection
  140.         view = itemSelection.currentView
  141.         if itemSelection.currentView is None:
  142.             return
  143.  
  144.         for item in view:
  145.             itemid = item.getID()
  146.             if itemSelection.isSelected(view, itemid) and item.isDownloaded():
  147.                 self.configure(view, itemid)
  148.                 break
  149.     
  150.     def enterPlayback(self):
  151.         if self.currentPlaylist is None:
  152.             self.configureWithSelection()
  153.         if self.currentPlaylist is not None:
  154.             startItem = self.currentPlaylist.cur()
  155.             if startItem is not None:
  156.                 self.playItem(startItem)
  157.         
  158.     def exitPlayback(self, switchDisplay=True):
  159.         self.reset()
  160.         if switchDisplay:
  161.             controller.selection.displayCurrentTabContent()
  162.     
  163.     def playPause(self):
  164.         videoDisplay = controller.videoDisplay
  165.         frame = controller.frame
  166.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  167.             videoDisplay.playPause()
  168.         else:
  169.             self.enterPlayback()
  170.  
  171.     def pause(self):
  172.         videoDisplay = controller.videoDisplay
  173.         frame = controller.frame
  174.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  175.             videoDisplay.pause()
  176.  
  177.     def removeItem(self, item):
  178.         if item.idExists():
  179.             item.executeExpire()
  180.  
  181.     def playItem(self, anItem):
  182.         try:
  183.             if self.currentItem:
  184.                 self.currentItem.onViewedCancel()
  185.             self.currentItem = None
  186.             while not os.path.exists(anItem.getVideoFilename()):
  187.                 logging.info ("movie file '%s' is missing, skipping to next",
  188.                               anItem.getVideoFilename())
  189.                 eventloop.addIdle(self.removeItem, "Remove deleted item", args=(anItem.item,))
  190.                 anItem = self.currentPlaylist.getNext()
  191.                 if anItem is None:
  192.                     self.stop()
  193.                     return
  194.  
  195.             self.currentItem = anItem
  196.             if anItem is not None:
  197.                 videoDisplay = controller.videoDisplay
  198.                 videoRenderer = videoDisplay.getRendererForItem(anItem)
  199.                 if videoRenderer is not None:
  200.                     self.playItemInternally(anItem, videoDisplay, videoRenderer)
  201.                 else:
  202.                     frame = controller.frame
  203.                     if frame.getDisplay(frame.mainDisplay) is videoDisplay:
  204.                         if videoDisplay.isFullScreen:
  205.                             videoDisplay.exitFullScreen()
  206.                         videoDisplay.stop()
  207.                     self.scheduleExternalPlayback(anItem)
  208.         except:
  209.             util.failedExn('when trying to play a video')
  210.             self.stop()
  211.  
  212.     def playItemInternally(self, anItem, videoDisplay, videoRenderer):
  213.         logging.info("Playing item with renderer: %s" % videoRenderer)
  214.         controller.videoDisplay.setExternal(False)
  215.         frame = controller.frame
  216.         if frame.getDisplay(frame.mainDisplay) is not videoDisplay:
  217.             frame.selectDisplay(videoDisplay, frame.mainDisplay)
  218.         videoDisplay.selectItem(anItem, videoRenderer)
  219.         if config.get(prefs.RESUME_VIDEOS_MODE) and anItem.resumeTime > 10:
  220.             videoDisplay.playFromTime(anItem.resumeTime)
  221.         else:
  222.             videoDisplay.play()
  223.         self.startUpdateVideoTime()
  224.  
  225.     def playItemExternally(self, itemID):
  226.         anItem = mapToPlaylistItem(db.getObjectByID(int(itemID)))
  227.         controller.videoInfoItem = anItem
  228.         newDisplay = TemplateDisplay('external-playback-continue','default')
  229.         frame = controller.frame
  230.         frame.selectDisplay(newDisplay, frame.mainDisplay)
  231.         return anItem
  232.         
  233.     def scheduleExternalPlayback(self, anItem):
  234.         controller.videoDisplay.setExternal(True)
  235.         controller.videoDisplay.stopOnDeselect = False
  236.         controller.videoInfoItem = anItem
  237.         newDisplay = TemplateDisplay('external-playback','default')
  238.         frame = controller.frame
  239.         frame.selectDisplay(newDisplay, frame.mainDisplay)
  240.         anItem.markItemSeen()
  241.  
  242.     def startUpdateVideoTime(self):
  243.         if not self.updateVideoTimeDC:
  244.             self.updateVideoTimeDC = eventloop.addTimeout(.5, self.updateVideoTime, "Update Video Time")
  245.  
  246.     def stopUpdateVideoTime(self):
  247.         if self.updateVideoTimeDC:
  248.             self.updateVideoTimeDC.cancel()
  249.             self.updateVideoTimeDC = None
  250.  
  251.     def updateVideoTime(self, repeat=True):
  252.         t = controller.videoDisplay.getCurrentTime()
  253.         if t != None and self.currentItem:
  254.             self.currentItem.setResumeTime(t)
  255.         if repeat:
  256.             self.updateVideoTimeDC = eventloop.addTimeout(.5, self.updateVideoTime, "Update Video Time")
  257.  
  258.     def stop(self, switchDisplay=True, markAsViewed=False):
  259.         controller.videoDisplay.setExternal(False)
  260.         if self.updateVideoTimeDC:
  261.             self.updateVideoTime(repeat=False)
  262.             self.stopUpdateVideoTime()
  263.         if self.currentItem:
  264.             self.currentItem.onViewedCancel()
  265.         self.currentItem = None
  266.         frame = controller.frame
  267.         videoDisplay = controller.videoDisplay
  268.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  269.             videoDisplay.stop()
  270.         self.exitPlayback(switchDisplay)
  271.  
  272.     def skip(self, direction, allowMovieReset=True):
  273.         frame = controller.frame
  274.         currentDisplay = frame.getDisplay(frame.mainDisplay)
  275.         if self.currentPlaylist is None:
  276.             self.stop()
  277.         elif (allowMovieReset and direction == -1
  278.                 and hasattr(currentDisplay, 'getCurrentTime') 
  279.                 and currentDisplay.getCurrentTime() > 2.0):
  280.             currentDisplay.goToBeginningOfMovie()
  281.         elif config.get(prefs.SINGLE_VIDEO_PLAYBACK_MODE) or self.justPlayOne:
  282.             self.stop()
  283.         else:
  284.             if direction == 1:
  285.                 nextItem = self.currentPlaylist.getNext()
  286.             else:
  287.                 nextItem = self.currentPlaylist.getPrev()
  288.             if nextItem is None:
  289.                 self.stop()
  290.             else:
  291.                 if self.updateVideoTimeDC:
  292.                     self.updateVideoTime(repeat=False)
  293.                     self.stopUpdateVideoTime()
  294.                 self.playItem(nextItem)
  295.  
  296.     def onMovieFinished(self):
  297.         self.stopUpdateVideoTime()
  298.         setToStart = False
  299.         if self.currentItem:
  300.             self.currentItem.setResumeTime(0)
  301.             if self.currentItem.getFeedURL() == 'dtv:singleFeed':
  302.                 setToStart = True
  303.         if setToStart:
  304.             frame = controller.frame
  305.             currentDisplay = frame.getDisplay(frame.mainDisplay)
  306.             currentDisplay.pause()
  307.             currentDisplay.goToBeginningOfMovie()
  308.             currentDisplay.pause()
  309.         else:
  310.             return self.skip(1, False)
  311.  
  312.  
  313. ###############################################################################
  314. #### Base class for displays                                               ####
  315. #### This must be defined before we import the frontend                    ####
  316. ###############################################################################
  317.  
  318. class Display:
  319.     "Base class representing a display in a MainFrame's right-hand pane."
  320.  
  321.     def __init__(self):
  322.         self.currentFrame = None # tracks the frame that currently has us selected
  323.  
  324.     def isSelected(self):
  325.         return self.currentFrame is not None
  326.  
  327.     def onSelected(self, frame):
  328.         "Called when the Display is shown in the given MainFrame."
  329.         pass
  330.  
  331.     def onDeselected(self, frame):
  332.         """Called when the Display is no longer shown in the given
  333.         MainFrame. This function is called on the Display losing the
  334.         selection before onSelected is called on the Display gaining the
  335.         selection."""
  336.         pass
  337.  
  338.     def onSelected_private(self, frame):
  339.         assert(self.currentFrame == None)
  340.         self.currentFrame = frame
  341.  
  342.     def onDeselected_private(self, frame):
  343.         assert(self.currentFrame == frame)
  344.         self.currentFrame = None
  345.  
  346.     # The MainFrame wants to know if we're ready to display (eg, if the
  347.     # a HTML display has finished loading its contents, so it can display
  348.     # immediately without flicker.) We're to call hook() when we're ready
  349.     # to be displayed.
  350.     def callWhenReadyToDisplay(self, hook):
  351.         hook()
  352.  
  353.     def cancel(self):
  354.         """Called when the Display is not shown because it is not ready yet
  355.         and another display will take its place"""
  356.         pass
  357.  
  358.     def getWatchable(self):
  359.         """Subclasses can implement this if they can return a database view
  360.         of watchable items"""
  361.         return None
  362.  
  363.  
  364. ###############################################################################
  365. #### Provides cross platform part of Video Display                         ####
  366. #### This must be defined before we import the frontend                    ####
  367. ###############################################################################
  368.  
  369. class VideoDisplayBase (Display):
  370.     
  371.     def __init__(self):
  372.         Display.__init__(self)
  373.         self.playbackController = None
  374.         self.volume = 1.0
  375.         self.previousVolume = 1.0
  376.         self.isPlaying = False
  377.         self.isPaused = False
  378.         self.isFullScreen = False
  379.         self.isExternal = False
  380.         self.stopOnDeselect = True
  381.         self.renderers = list()
  382.         self.activeRenderer = None
  383.  
  384.     def initRenderers(self):
  385.         pass
  386.  
  387.     def setExternal(self, external):
  388.         self.isExternal = external
  389.  
  390.     def fillMovieData (self, filename, movie_data, callback):
  391.         for renderer in self.renderers:
  392.             success = renderer.fillMovieData(filename, movie_data)
  393.             if success:
  394.                 callback ()
  395.                 return
  396.         callback ()
  397.         
  398.     def getRendererForItem(self, anItem):
  399.         for renderer in self.renderers:
  400.             if renderer.canPlayItem(anItem):
  401.                 return renderer
  402.         return None
  403.  
  404.     def canPlayItem(self, anItem):
  405.         return self.getRendererForItem(anItem) is not None
  406.     
  407.     def canPlayFile(self, filename):
  408.         for renderer in self.renderers:
  409.             if renderer.canPlayFile(filename):
  410.                 return True
  411.         return False
  412.     
  413.     def selectItem(self, anItem, renderer):
  414.         self.stopOnDeselect = True
  415.         controller.videoInfoItem = anItem
  416.         templ = TemplateDisplay('video-info', 'default')
  417.         area = controller.frame.videoInfoDisplay
  418.         controller.frame.selectDisplay(templ, area)
  419.  
  420.         self.setActiveRenderer(renderer)
  421.         self.activeRenderer.selectItem(anItem)
  422.         self.activeRenderer.setVolume(self.getVolume())
  423.  
  424.     def setActiveRenderer (self, renderer):
  425.         self.activeRenderer = renderer
  426.  
  427.     def reset(self):
  428.         self.isPlaying = False
  429.         self.isPaused = False
  430.         self.stopOnDeselect = True
  431.         if self.activeRenderer is not None:
  432.             self.activeRenderer.reset()
  433.         self.activeRenderer = None
  434.  
  435.     def goToBeginningOfMovie(self):
  436.         if self.activeRenderer is not None:
  437.             self.activeRenderer.goToBeginningOfMovie()
  438.  
  439.     def playPause(self):
  440.         if self.isPlaying:
  441.             self.pause()
  442.         else:
  443.             self.play()
  444.  
  445.     def playFromTime(self, startTime):
  446.         if self.activeRenderer is not None:
  447.             self.activeRenderer.playFromTime(startTime)
  448.         self.isPlaying = True
  449.         self.isPaused = False
  450.  
  451.     def play(self):
  452.         if self.activeRenderer is not None:
  453.             self.activeRenderer.play()
  454.         self.isPlaying = True
  455.         self.isPaused = False
  456.  
  457.     def pause(self):
  458.         if self.activeRenderer is not None:
  459.             self.activeRenderer.pause()
  460.         self.isPlaying = False
  461.         self.isPaused = True
  462.  
  463.     def stop(self):
  464.         if self.isFullScreen:
  465.             self.exitFullScreen()
  466.         if self.activeRenderer is not None:
  467.             self.activeRenderer.stop()
  468.         self.reset()
  469.  
  470.     def goFullScreen(self):
  471.         self.isFullScreen = True
  472.         if not self.isPlaying:
  473.             self.play()
  474.  
  475.     def exitFullScreen(self):
  476.         self.isFullScreen = False
  477.  
  478.     def getCurrentTime(self):
  479.         if self.activeRenderer is not None:
  480.             return self.activeRenderer.getCurrentTime()
  481.         return None
  482.  
  483.     def setCurrentTime(self, seconds):
  484.         if self.activeRenderer is not None:
  485.             self.activeRenderer.setCurrentTime(seconds)
  486.  
  487.     def getProgress(self):
  488.         if self.activeRenderer is not None:
  489.             return self.activeRenderer.getProgress()
  490.         return 0.0
  491.  
  492.     def setProgress(self, progress):
  493.         if self.activeRenderer is not None:
  494.             return self.activeRenderer.setProgress(progress)
  495.  
  496.     def getDuration(self):
  497.         if self.activeRenderer is not None:
  498.             return self.activeRenderer.getDuration()
  499.         return None
  500.  
  501.     def setVolume(self, level):
  502.         if level > 1.0:
  503.             level = 1.0
  504.         if level < 0.0:
  505.             level = 0.0
  506.         self.volume = level
  507.         config.set(prefs.VOLUME_LEVEL, level)
  508.         if self.activeRenderer is not None:
  509.             self.activeRenderer.setVolume(level)
  510.  
  511.     def getVolume(self):
  512.         return self.volume
  513.  
  514.     def muteVolume(self):
  515.         self.previousVolume = self.getVolume()
  516.         self.setVolume(0.0)
  517.  
  518.     def restoreVolume(self):
  519.         self.setVolume(self.previousVolume)
  520.  
  521.     def onDeselected(self, frame):
  522.         if self.stopOnDeselect and (self.isPlaying or self.isPaused):
  523.             controller.playbackController.stop(False)
  524.     
  525. ###############################################################################
  526. #### Video renderer base class                                             ####
  527. ###############################################################################
  528.  
  529. class VideoRenderer:
  530.         
  531.     def __init__(self):
  532.         self.interactivelySeeking = False
  533.     
  534.     def canPlayItem(self, anItem):
  535.         return self.canPlayFile (anItem.getVideoFilename())
  536.     
  537.     def canPlayFile(self, filename):
  538.         return False
  539.  
  540.     def fillMovieData(self, filename, movie_data):
  541.         return False
  542.     
  543.     def getDisplayTime(self):
  544.         seconds = self.getCurrentTime()
  545.         return util.formatTimeForUser(seconds)
  546.         
  547.     def getDisplayDuration(self):
  548.         seconds = self.getDuration()
  549.         return util.formatTimeForUser(seconds)
  550.  
  551.     def getDisplayRemainingTime(self):
  552.         seconds = abs(self.getCurrentTime() - self.getDuration())
  553.         return util.formatTimeForUser(seconds, -1)
  554.  
  555.     def getProgress(self):
  556.         duration = self.getDuration()
  557.         if duration == 0 or duration == None:
  558.             return 0.0
  559.         return self.getCurrentTime() / duration
  560.  
  561.     def setProgress(self, progress):
  562.         if progress > 1.0:
  563.             progress = 1.0
  564.         if progress < 0.0:
  565.             progress = 0.0
  566.         self.setCurrentTime(self.getDuration() * progress)
  567.  
  568.     def selectItem(self, anItem):
  569.         self.selectFile (anItem.getVideoFilename())
  570.  
  571.     def selectFile(self, filename):
  572.         pass
  573.         
  574.     def reset(self):
  575.         pass
  576.  
  577.     def setCurrentTime(self, seconds):
  578.         pass
  579.  
  580.     def getDuration(self):
  581.         return 0.0
  582.  
  583.     def setVolume(self, level):
  584.         pass
  585.                 
  586.     def goToBeginningOfMovie(self):
  587.         pass
  588.  
  589.     def getCurrentTime(self):
  590.         return None
  591.         
  592.     def playFromTime(self, position):
  593.         self.play()
  594.         self.setCurrentTime(position)
  595.         
  596.     def play(self):
  597.         pass
  598.         
  599.     def pause(self):
  600.         pass
  601.         
  602.     def stop(self):
  603.         pass
  604.     
  605.     def getRate(self):
  606.         return 1.0
  607.     
  608.     def setRate(self, rate):
  609.         pass
  610.  
  611.     def movieDataProgramInfo(self, videoPath, thumbnailPath):
  612.         raise NotImplementedError()
  613.         
  614. # We can now safely import the frontend module
  615. import frontend
  616.  
  617. ###############################################################################
  618. #### The main application controller object, binding model to view         ####
  619. ###############################################################################
  620.  
  621. class Controller (frontend.Application):
  622.  
  623.     def __init__(self):
  624.         global controller
  625.         global delegate
  626.         frontend.Application.__init__(self)
  627.         assert controller is None
  628.         assert delegate is None
  629.         controller = self
  630.         delegate = frontend.UIBackendDelegate()
  631.         self.frame = None
  632.         self.inQuit = False
  633.         self.guideURL = None
  634.         self.guide = None
  635.         self.initial_feeds = False # True if this is the first run and there's an initial-feeds.democracy file.
  636.         self.finishedStartup = False
  637.         self.idlingNotifier = None
  638.         self.gatheredVideos = None
  639.         self.sendingCrashReport = 0
  640.         self.librarySearchTerm = None
  641.         self.newVideosSearchTerm = None
  642.  
  643.     ### Startup and shutdown ###
  644.  
  645.     def onStartup(self, gatheredVideos=None):
  646.         logging.info ("Starting up %s", config.get(prefs.LONG_APP_NAME))
  647.         logging.info ("Version:    %s", config.get(prefs.APP_VERSION))
  648.         logging.info ("Revision:   %s", config.get(prefs.APP_REVISION))
  649.         logging.info ("Builder:    %s", config.get(prefs.BUILD_MACHINE))
  650.         logging.info ("Build Time: %s", config.get(prefs.BUILD_TIME))
  651.  
  652.         util.print_mem_usage("Pre everything memory check")
  653.         
  654.         logging.info ("Loading preferences...")
  655.  
  656.         config.load()
  657.         config.addChangeCallback(self.configDidChange)
  658.         
  659.         global delegate
  660.         feed.setDelegate(delegate)
  661.         feed.setSortFunc(sorts.item)
  662.         autoupdate.setDelegate(delegate)
  663.         database.setDelegate(delegate)
  664.         dialogs.setDelegate(delegate)
  665.         
  666.         if not config.get(prefs.STARTUP_TASKS_DONE):
  667.             logging.info ("Showing startup dialog...")
  668.             delegate.performStartupTasks(self.finishStartup)
  669.             config.set(prefs.STARTUP_TASKS_DONE, True)
  670.             config.save()
  671.         else:
  672.             self.finishStartup(gatheredVideos)
  673.         logging.info ("Starting event loop thread")
  674.         eventloop.startup()
  675.  
  676.     def finishStartup(self, gatheredVideos=None):
  677.         self.gatheredVideos = gatheredVideos
  678.         eventloop.addUrgentCall(self.initializeDatabase, "Initializing database")
  679.  
  680.     @startupFunction
  681.     def initializeDatabase(self):
  682.         try:
  683.             views.initialize()
  684.             util.print_mem_usage("Pre-database memory check:")
  685.             logging.info ("Restoring database...")
  686.             database.defaultDatabase.liveStorage = storedatabase.LiveStorage()
  687.             db.recomputeFilters()
  688.             eventloop.addUrgentCall(self.checkMoviesDirectoryGone, 
  689.                     "checking movies directory")
  690.         except databaseupgrade.DatabaseTooNewError:
  691.             title = _("Database too new")
  692.             description = Template(_("""\
  693. You have a database that was saved with a newer version of $shortAppName. \
  694. You must download the latest version of $shortAppName and run that.""")).substitute(shortAppName = config.get(prefs.SHORT_APP_NAME))
  695.             def callback(dialog):
  696.                 eventloop.quit()
  697.                 frontend.quit(True)
  698.             dialogs.MessageBoxDialog(title, description).run(callback)
  699.  
  700.     @startupFunction
  701.     def checkMoviesDirectoryGone(self):
  702.         if not self.moviesDirectoryGone():
  703.             eventloop.addUrgentCall(self.finalizeStartup, "finalizing startup")
  704.             return
  705.  
  706.         title = _("Video Directory Missing")
  707.         description = _("""
  708. Miro can't find your primary video directory.  This may be because it's \
  709. located on an external drive that is currently disconnected.
  710.  
  711. If you continue, the video directory will be reset to a location on this \
  712. drive (this will cause you to lose some details about the videos on the \
  713. external drive).  You can also quit, connect the drive, and relaunch Miro.""")
  714.         dialog = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_QUIT,
  715.                 dialogs.BUTTON_LAUNCH_MIRO)
  716.         def callback(dialog):
  717.             if dialog.choice == dialogs.BUTTON_LAUNCH_MIRO:
  718.                 eventloop.addUrgentCall(self.finalizeStartup, "finalizing startup")
  719.             else:
  720.                 eventloop.quit()
  721.                 frontend.quit(True)
  722.         dialog.run(callback)
  723.  
  724.     @startupFunction
  725.     def finalizeStartup(self):
  726.         downloader.startupDownloader()
  727.  
  728.         util.print_mem_usage("Post-downloader memory check")
  729.  
  730.         self.setupGlobalFeed(u'dtv:manualFeed', initiallyAutoDownloadable=False)
  731.         self.setupGlobalFeed(u'dtv:singleFeed', initiallyAutoDownloadable=False)
  732.  
  733.         # Set up the search objects
  734.         self.setupGlobalFeed(u'dtv:search', initiallyAutoDownloadable=False)
  735.         self.setupGlobalFeed(u'dtv:searchDownloads')
  736.  
  737.         # Set up tab list
  738.         tabs.reloadStaticTabs()
  739.         try:
  740.             channelTabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  741.         except LookupError:
  742.             logging.info ("Creating channel tab order")
  743.             channelTabOrder = tabs.TabOrder(u'channel')
  744.         try:
  745.             playlistTabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  746.         except LookupError:
  747.             logging.info ("Creating playlist tab order")
  748.             playlistTabOrder = tabs.TabOrder(u'playlist')
  749.  
  750.         # Set up search engines
  751.         searchengines.createEngines()
  752.  
  753.         # This will create the ChannelGuide object, if necessary
  754.         _getThemeHistory()
  755.  
  756.         # Keep a ref of the 'new' and 'download' tabs, we'll need'em later
  757.         self.newTab = None
  758.         self.downloadTab = None
  759.         for tab in views.allTabs:
  760.             if tab.tabTemplateBase == 'newtab':
  761.                 self.newTab = tab
  762.             elif tab.tabTemplateBase == 'downloadtab':
  763.                 self.downloadTab = tab
  764.         views.unwatchedItems.addAddCallback(self.onUnwatchedItemsCountChange)
  765.         views.unwatchedItems.addRemoveCallback(self.onUnwatchedItemsCountChange)
  766.         views.downloadingItems.addAddCallback(self.onDownloadingItemsCountChange)
  767.         views.downloadingItems.addRemoveCallback(self.onDownloadingItemsCountChange)
  768.         self.onUnwatchedItemsCountChange(None, None)
  769.         self.onDownloadingItemsCountChange(None, None)
  770.  
  771.         # If we're missing the file system videos feed, create it
  772.         self.setupGlobalFeed(u'dtv:directoryfeed')
  773.  
  774.         # Start the automatic downloader daemon
  775.         logging.info ("Spawning auto downloader...")
  776.         autodler.startDownloader()
  777.  
  778.         # Start the idle notifier daemon
  779.         if config.get(prefs.LIMIT_UPSTREAM) is True:
  780.             logging.info ("Spawning idle notifier")
  781.             self.idlingNotifier = idlenotifier.IdleNotifier(self)
  782.             self.idlingNotifier.start()
  783.  
  784.         # Set up the playback controller
  785.         self.playbackController = frontend.PlaybackController()
  786.  
  787.         util.print_mem_usage("Pre-UI memory check")
  788.  
  789.         # Put up the main frame
  790.         logging.info ("Displaying main frame...")
  791.         self.frame = frontend.MainFrame(self)
  792.  
  793.         logging.info ("Creating video display...")
  794.         # Set up the video display
  795.         self.videoDisplay = frontend.VideoDisplay()
  796.         self.videoDisplay.initRenderers()
  797.         self.videoDisplay.playbackController = self.playbackController
  798.         self.videoDisplay.setVolume(config.get(prefs.VOLUME_LEVEL))
  799.         util.print_mem_usage("Post-UI memory check")
  800.  
  801.         # create our selection handler
  802.         
  803.         self.selection = selection.SelectionHandler()
  804.  
  805.         self.selection.selectFirstTab()
  806.  
  807.         if self.initial_feeds:
  808.             views.feedTabs.resetCursor()
  809.             tab = views.feedTabs.getNext()
  810.             if tab is not None:
  811.                 self.selection.selectTabByObject(tab.obj)
  812.  
  813.         util.print_mem_usage("Post-selection memory check")
  814.  
  815.         # Reconnect items to downloaders.
  816.         item.reconnectDownloaders()
  817.  
  818.         util.print_mem_usage("Post-item reconnect memory check")
  819.  
  820.         eventloop.addTimeout (3, autoupdate.checkForUpdates, "Check for updates")
  821.         feed.expireItems()
  822.  
  823.         self.tabDisplay = TemplateDisplay('tablist', 'default',
  824.                 playlistTabOrder=playlistTabOrder,
  825.                 channelTabOrder=channelTabOrder)
  826.         self.frame.selectDisplay(self.tabDisplay, self.frame.channelsDisplay)
  827.  
  828.         # If we have newly available items, provide feedback
  829.         self.updateAvailableItemsCountFeedback()
  830.  
  831.         # Now adding the video files we possibly gathered from the startup
  832.         # dialog
  833.         if self.gatheredVideos is not None and len(self.gatheredVideos) > 0:
  834.             singleclick.resetCommandLineView()
  835.             for v in self.gatheredVideos:
  836.                 try:
  837.                     singleclick.addVideo(v)
  838.                 except Exception, e:
  839.                     logging.info ("error while adding file %s", v)
  840.                     logging.info (e)
  841.  
  842.         util.print_mem_usage("Pre single-click memory check")
  843.  
  844.         # Use an idle for parseCommandLineArgs because the frontend may
  845.         # have put in idle calls to do set up video playback or similar
  846.         # things.
  847.         eventloop.addIdle(singleclick.parseCommandLineArgs, 
  848.                 'parse command line')
  849.  
  850.         util.print_mem_usage("Post single-click memory check")
  851.  
  852.         starttime = clock()
  853.         iconcache.clearOrphans()
  854.         logging.timing ("Icon clear: %.3f", clock() - starttime)
  855.         logging.info ("Starting movie data updates")
  856.         moviedata.movieDataUpdater.startThread()
  857.  
  858.         logging.info ("Finished startup sequence")
  859.         self.finishStartupSequence()
  860.  
  861.     def finishStartupSequence(self):
  862.         self.finishedStartup = True
  863.         frontend.Application.finishStartupSequence(self)
  864.  
  865.     def setupGlobalFeed(self, url, *args, **kwargs):
  866.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  867.         try:
  868.             if feedView.len() == 0:
  869.                 logging.info ("Spawning global feed %s", url)
  870.                 # FIXME - variable d never gets used.
  871.                 d = feed.Feed(url, *args, **kwargs)
  872.             elif feedView.len() > 1:
  873.                 allFeeds = [f for f in feedView]
  874.                 for extra in allFeeds[1:]:
  875.                     extra.remove()
  876.                 util.failed("Too many db objects for %s" % url)
  877.         finally:
  878.             feedView.unlink()
  879.  
  880.     def moviesDirectoryGone(self):
  881.         movies_dir = config.get(prefs.MOVIES_DIRECTORY)
  882.         if not movies_dir.endswith(os.path.sep):
  883.             movies_dir += os.path.sep
  884.         try:
  885.             contents = os.listdir(movies_dir)
  886.         except OSError:
  887.             # We can't access the directory.  Seems like it's gone.
  888.             return True
  889.         if contents != []:
  890.             # There's something inside the directory consider it present  (even
  891.             # if all our items are missing.
  892.             return False
  893.         # make sure that we have actually downloaded something into the movies
  894.         # directory. 
  895.         for downloader in views.remoteDownloads:
  896.             if (downloader.isFinished() and
  897.                     downloader.getFilename().startswith(movies_dir)):
  898.                 return True
  899.         return False
  900.  
  901.     def getGlobalFeed(self, url):
  902.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  903.         rv = feedView[0]
  904.         feedView.unlink()
  905.         return rv
  906.  
  907.     def removeGlobalFeed(self, url):
  908.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  909.         feedView.resetCursor()
  910.         nextfeed = feedView.getNext()
  911.         feedView.unlink()
  912.         if nextfeed is not None:
  913.             logging.info ("Removing global feed %s", url)
  914.             nextfeed.remove()
  915.  
  916.     def copyCurrentFeedURL(self):
  917.         tabs = self.selection.getSelectedTabs()
  918.         if len(tabs) == 1 and tabs[0].isFeed():
  919.             delegate.copyTextToClipboard(tabs[0].obj.getURL())
  920.  
  921.     def recommendCurrentFeed(self):
  922.         tabs = self.selection.getSelectedTabs()
  923.         if len(tabs) == 1 and tabs[0].isFeed():
  924.             # See also dynamic.js if changing this URL
  925.             feed = tabs[0].obj
  926.             query = urllib.urlencode({'url': feed.getURL(), 'title': feed.getTitle()})
  927.             delegate.openExternalURL('http://www.videobomb.com/democracy_channel/email_friend?%s' % (query, ))
  928.  
  929.     def copyCurrentItemURL(self):
  930.         tabs = self.selection.getSelectedItems()
  931.         if len(tabs) == 1 and isinstance(tabs[0], item.Item):
  932.             url = tabs[0].getURL()
  933.             if url:
  934.                 delegate.copyTextToClipboard(url)
  935.  
  936.     def selectAllItems(self):
  937.         self.selection.itemListSelection.selectAll()
  938.         self.selection.setTabListActive(False)
  939.  
  940.     def removeCurrentSelection(self):
  941.         if self.selection.tabListActive:
  942.             selection = self.selection.tabListSelection
  943.         else:
  944.             selection = self.selection.itemListSelection
  945.         seltype = selection.getType()
  946.         if seltype == 'channeltab':
  947.             self.removeCurrentFeed()
  948.         elif seltype == 'addedguidetab':
  949.             self.removeCurrentGuide()
  950.         elif seltype == 'playlisttab':
  951.             self.removeCurrentPlaylist()
  952.         elif seltype == 'item':
  953.             self.removeCurrentItems()
  954.  
  955.     def removeCurrentFeed(self):
  956.         if self.selection.tabListSelection.getType() == 'channeltab':
  957.             feeds = [t.obj for t in self.selection.getSelectedTabs()]
  958.             self.removeFeeds(feeds)
  959.  
  960.     def removeCurrentGuide(self):
  961.         if self.selection.tabListSelection.getType() == 'addedguidetab':
  962.             guides = [t.obj for t in self.selection.getSelectedTabs()]
  963.             if len(guides) != 1:
  964.                 raise AssertionError("Multiple guides selected")
  965.             self.removeGuide(guides[0])
  966.  
  967.     def removeCurrentPlaylist(self):
  968.         if self.selection.tabListSelection.getType() == 'playlisttab':
  969.             playlists = [t.obj for t in self.selection.getSelectedTabs()]
  970.             self.removePlaylists(playlists)
  971.  
  972.     def removeCurrentItems(self):
  973.         if self.selection.itemListSelection.getType() != 'item':
  974.             return
  975.         selected = self.selection.getSelectedItems()
  976.         if self.selection.tabListSelection.getType() != 'playlisttab':
  977.             removable = [i for i in selected if (i.isDownloaded() or i.isExternal()) ]
  978.             if removable:
  979.                 item.expireItems(removable)
  980.         else:
  981.             playlist = self.selection.getSelectedTabs()[0].obj
  982.             for i in selected:
  983.                 playlist.removeItem(i)
  984.  
  985.     def renameCurrentTab(self, typeCheckList=None):
  986.         selected = self.selection.getSelectedTabs()
  987.         if len(selected) != 1:
  988.             return
  989.         obj = selected[0].obj
  990.         if typeCheckList is None:
  991.             typeCheckList = (playlist.SavedPlaylist, folder.ChannelFolder,
  992.                 folder.PlaylistFolder, feed.Feed)
  993.         if obj.__class__ in typeCheckList:
  994.             obj.rename()
  995.         else:
  996.             logging.warning ("Bad object type in renameCurrentTab() %s", obj.__class__)
  997.  
  998.     def renameCurrentChannel(self):
  999.         self.renameCurrentTab(typeCheckList=[feed.Feed, folder.ChannelFolder])
  1000.  
  1001.     def renameCurrentPlaylist(self):
  1002.         self.renameCurrentTab(typeCheckList=[playlist.SavedPlaylist,
  1003.                 folder.PlaylistFolder])
  1004.  
  1005.     def downloadCurrentItems(self):
  1006.         selected = self.selection.getSelectedItems()
  1007.         downloadable = [i for i in selected if i.isDownloadable() ]
  1008.         for item in downloadable:
  1009.             item.download()
  1010.  
  1011.     def stopDownloadingCurrentItems(self):
  1012.         selected = self.selection.getSelectedItems()
  1013.         downloading = [i for i in selected if i.getState() == 'downloading']
  1014.         for item in downloading:
  1015.             item.expire()
  1016.  
  1017.     def pauseDownloadingCurrentItems(self):
  1018.         selected = self.selection.getSelectedItems()
  1019.         downloading = [i for i in selected if i.getState() == 'downloading']
  1020.         for item in downloading:
  1021.             item.pause()
  1022.  
  1023.     def updateCurrentFeed(self):
  1024.         for tab in self.selection.getSelectedTabs():
  1025.             if tab.isFeed():
  1026.                 tab.obj.update()
  1027.  
  1028.     def updateAllFeeds(self):
  1029.         for f in views.feeds:
  1030.             f.update()
  1031.  
  1032.     def removeGuide(self, guide):
  1033.         if guide.getDefault():
  1034.             logging.warning ("attempt to remove default guide")
  1035.             return
  1036.         title = _('Remove %s') % guide.getTitle()
  1037.         description = _("Are you sure you want to remove the guide %s?") % (guide.getTitle(),)
  1038.         dialog = dialogs.ChoiceDialog(title, description, 
  1039.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1040.         def dialogCallback(dialog):
  1041.             if guide.idExists() and dialog.choice == dialogs.BUTTON_YES:
  1042.                 guide.remove()
  1043.         dialog.run(dialogCallback)
  1044.  
  1045.     def removePlaylist(self, playlist):
  1046.         return self.removePlaylists([playlist])
  1047.  
  1048.     def removePlaylists(self, playlists):
  1049.         if len(playlists) == 1:
  1050.             title = _('Remove %s') % playlists[0].getTitle()
  1051.             description = _("Are you sure you want to remove %s") % \
  1052.                     playlists[0].getTitle()
  1053.         else:
  1054.             title = _('Remove %s channels') % len(playlists)
  1055.             description = \
  1056.                     _("Are you sure you want to remove these %s playlists") % \
  1057.                     len(playlists)
  1058.         dialog = dialogs.ChoiceDialog(title, description, 
  1059.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1060.         def dialogCallback(dialog):
  1061.             if dialog.choice == dialogs.BUTTON_YES:
  1062.                 for playlist in playlists:
  1063.                     if playlist.idExists():
  1064.                         playlist.remove()
  1065.         dialog.run(dialogCallback)
  1066.  
  1067.     def removeFeed(self, feed):
  1068.         return self.removeFeeds([feed])
  1069.  
  1070.     def removeFeeds(self, feeds):
  1071.         downloads = False
  1072.         downloading = False
  1073.         allDirectories = True
  1074.         for feed in feeds:
  1075.             # We only care about downloaded items in non directory feeds.
  1076.             if isinstance(feed, folder.ChannelFolder) or not feed.getURL().startswith("dtv:directoryfeed"):
  1077.                 allDirectories = False
  1078.                 if feed.hasDownloadedItems():
  1079.                     downloads = True
  1080.                     break
  1081.                 if feed.hasDownloadingItems():
  1082.                     downloading = True
  1083.         if downloads:
  1084.             self.removeFeedsWithDownloads(feeds)
  1085.         elif downloading:
  1086.             self.removeFeedsWithDownloading(feeds)
  1087.         elif allDirectories:
  1088.             self.removeDirectoryFeeds(feeds)
  1089.         else:
  1090.             self.removeFeedsNormal(feeds)
  1091.  
  1092.     def removeFeedsWithDownloads(self, feeds):
  1093.         if len(feeds) == 1:
  1094.             title = _('Remove %s') % feeds[0].getTitle()
  1095.             description = _("""\
  1096. What would you like to do with the videos in this channel that you've \
  1097. downloaded?""")
  1098.         else:
  1099.             title = _('Remove %s channels') % len(feeds)
  1100.             description = _("""\
  1101. What would you like to do with the videos in these channels that you've \
  1102. downloaded?""")
  1103.         dialog = dialogs.ThreeChoiceDialog(title, description, 
  1104.                 dialogs.BUTTON_KEEP_VIDEOS, dialogs.BUTTON_DELETE_VIDEOS,
  1105.                 dialogs.BUTTON_CANCEL)
  1106.         def dialogCallback(dialog):
  1107.             if dialog.choice == dialogs.BUTTON_KEEP_VIDEOS:
  1108.                 manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1109.                 for feed in feeds:
  1110.                     if feed.idExists():
  1111.                         feed.remove(moveItemsTo=manualFeed)
  1112.             elif dialog.choice == dialogs.BUTTON_DELETE_VIDEOS:
  1113.                 for feed in feeds:
  1114.                     if feed.idExists():
  1115.                         feed.remove()
  1116.         dialog.run(dialogCallback)
  1117.  
  1118.     def removeFeedsWithDownloading(self, feeds):
  1119.         if len(feeds) == 1:
  1120.             title = _('Remove %s') % feeds[0].getTitle()
  1121.             description = _("""\
  1122. Are you sure you want to remove %s?  Any downloads in progress will \
  1123. be canceled.""") % feeds[0].getTitle()
  1124.         else:
  1125.             title = _('Remove %s channels') % len(feeds)
  1126.             description = _("""\
  1127. Are you sure you want to remove these %s channels?  Any downloads in \
  1128. progress will be canceled.""") % len(feeds)
  1129.         dialog = dialogs.ChoiceDialog(title, description, 
  1130.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1131.         def dialogCallback(dialog):
  1132.             if dialog.choice == dialogs.BUTTON_YES:
  1133.                 for feed in feeds:
  1134.                     if feed.idExists():
  1135.                         feed.remove()
  1136.         dialog.run(dialogCallback)
  1137.  
  1138.     def removeFeedsNormal(self, feeds):
  1139.         if len(feeds) == 1:
  1140.             title = _('Remove %s') % feeds[0].getTitle()
  1141.             description = _("""\
  1142. Are you sure you want to remove %s?""") % feeds[0].getTitle()
  1143.         else:
  1144.             title = _('Remove %s channels') % len(feeds)
  1145.             description = _("""\
  1146. Are you sure you want to remove these %s channels?""") % len(feeds)
  1147.         dialog = dialogs.ChoiceDialog(title, description, 
  1148.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1149.         def dialogCallback(dialog):
  1150.             if dialog.choice == dialogs.BUTTON_YES:
  1151.                 for feed in feeds:
  1152.                     if feed.idExists():
  1153.                         feed.remove()
  1154.         dialog.run(dialogCallback)
  1155.  
  1156.     def removeDirectoryFeeds(self, feeds):
  1157.         if len(feeds) == 1:
  1158.             title = _('Stop watching %s') % feeds[0].getTitle()
  1159.             description = _("""\
  1160. Are you sure you want to stop watching %s?""") % feeds[0].getTitle()
  1161.         else:
  1162.             title = _('Stop watching %s directories') % len(feeds)
  1163.             description = _("""\
  1164. Are you sure you want to stop watching these %s directories?""") % len(feeds)
  1165.         dialog = dialogs.ChoiceDialog(title, description, 
  1166.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1167.         def dialogCallback(dialog):
  1168.             if dialog.choice == dialogs.BUTTON_YES:
  1169.                 for feed in feeds:
  1170.                     if feed.idExists():
  1171.                         feed.remove()
  1172.         dialog.run(dialogCallback)
  1173.  
  1174.     def playView(self, view, firstItemId=None, justPlayOne=False):
  1175.         self.playbackController.configure(view, firstItemId, justPlayOne)
  1176.         self.playbackController.enterPlayback()
  1177.  
  1178.     def downloaderShutdown(self):
  1179.         logging.info ("Closing Database...")
  1180.         database.defaultDatabase.liveStorage.close()
  1181.         logging.info ("Shutting down event loop")
  1182.         eventloop.quit()
  1183.         logging.info ("Shutting down frontend")
  1184.         frontend.quit()
  1185.  
  1186.     @eventloop.asUrgent
  1187.     def quit(self):
  1188.         global delegate
  1189.         if self.inQuit:
  1190.             return
  1191.         downloadsCount = views.downloadingItems.len()
  1192.             
  1193.         if (downloadsCount > 0 and config.get(prefs.WARN_IF_DOWNLOADING_ON_QUIT)) or (self.sendingCrashReport > 0):
  1194.             title = _("Are you sure you want to quit?")
  1195.             if self.sendingCrashReport > 0:
  1196.                 message = _("Miro is still uploading your crash report. If you quit now the upload will be canceled.  Quit Anyway?")
  1197.                 dialog = dialogs.ChoiceDialog(title, message,
  1198.                                               dialogs.BUTTON_QUIT,
  1199.                                               dialogs.BUTTON_CANCEL)
  1200.             else:
  1201.                 message = ngettext ("You have %d download still in progress.  Quit Anyway?", 
  1202.                                     "You have %d downloads still in progress.  Quit Anyway?", 
  1203.                                     downloadsCount) % (downloadsCount,)
  1204.                 warning = _ ("Warn me when I attempt to quit with downloads in progress")
  1205.                 dialog = dialogs.CheckboxDialog(title, message, warning, True,
  1206.                         dialogs.BUTTON_QUIT, dialogs.BUTTON_CANCEL)
  1207.  
  1208.             def callback(dialog):
  1209.                 if dialog.choice == dialogs.BUTTON_QUIT:
  1210.                     if isinstance(dialog, dialogs.CheckboxDialog):
  1211.                         config.set(prefs.WARN_IF_DOWNLOADING_ON_QUIT,
  1212.                                    dialog.checkbox_value)
  1213.                     self.quitStage2()
  1214.                 else:
  1215.                     self.inQuit = False
  1216.             dialog.run(callback)
  1217.             self.inQuit = True
  1218.         else:
  1219.             self.quitStage2()
  1220.  
  1221.     def quitStage2(self):
  1222.         logging.info ("Shutting down Downloader...")
  1223.         downloader.shutdownDownloader(self.downloaderShutdown)
  1224.  
  1225.     @eventloop.asUrgent
  1226.     def setGuideURL(self, guideURL):
  1227.         """Change the URL of the current channel guide being displayed.  If no
  1228.         guide is being display, pass in None.
  1229.  
  1230.         This method must be called from the onSelectedTabChange in the
  1231.         platform code.  URLs are legal within guideURL will be allow
  1232.         through in onURLLoad().
  1233.         """
  1234.         self.guide = None
  1235.         if guideURL is not None:
  1236.             self.guideURL = guideURL
  1237.             for guideObj in views.guides:
  1238.                 if guideObj.getURL() == controller.guideURL:
  1239.                     self.guide = guideObj
  1240.         else:
  1241.             self.guideURL = None
  1242.  
  1243.     @eventloop.asIdle
  1244.     def setLastVisitedGuideURL(self, url):
  1245.         selectedTabs = self.selection.getSelectedTabs()
  1246.         selectedObjects = [t.obj for t in selectedTabs]
  1247.         if (len(selectedTabs) != 1 or 
  1248.                 not isinstance(selectedObjects[0], guide.ChannelGuide)):
  1249.             logging.warn("setLastVisitedGuideURL called, but a channelguide "
  1250.                     "isn't selected.  Selection: %s" % selectedObjects)
  1251.             return
  1252.         if selectedObjects[0].isPartOfGuide(url) and (
  1253.             url.startswith(u"http://") or url.startswith(u"https://")):
  1254.             selectedObjects[0].lastVisitedURL = url
  1255.             selectedObjects[0].extendHistory(url)
  1256.         else:
  1257.             logging.warn("setLastVisitedGuideURL called, but the guide is no "
  1258.                     "longer selected")
  1259.  
  1260.     def onShutdown(self):
  1261.         try:
  1262.             eventloop.join()        
  1263.             logging.info ("Saving preferences...")
  1264.             config.save()
  1265.  
  1266. #             logging.info ("Removing search feed")
  1267. #             TemplateActionHandler(None, None).resetSearch()
  1268. #             self.removeGlobalFeed('dtv:search')
  1269.  
  1270.             logging.info ("Shutting down icon cache updates")
  1271.             iconcache.iconCacheUpdater.shutdown()
  1272.             logging.info ("Shutting down movie data updates")
  1273.             moviedata.movieDataUpdater.shutdown()
  1274.  
  1275. #             logging.info ("Removing static tabs...")
  1276. #             views.allTabs.unlink() 
  1277. #             tabs.removeStaticTabs()
  1278.  
  1279.             if self.idlingNotifier is not None:
  1280.                 logging.info ("Shutting down IdleNotifier")
  1281.                 self.idlingNotifier.join()
  1282.  
  1283.             logging.info ("Done shutting down.")
  1284.             logging.info ("Remaining threads are:")
  1285.             for thread in threading.enumerate():
  1286.                 logging.info ("%s", thread)
  1287.  
  1288.         except:
  1289.             util.failedExn("while shutting down")
  1290.             frontend.exit(1)
  1291.  
  1292.     ### Handling config/prefs changes
  1293.     
  1294.     def configDidChange(self, key, value):
  1295.         if key is prefs.LIMIT_UPSTREAM.key:
  1296.             if value is False:
  1297.                 # The Windows version can get here without creating an
  1298.                 # idlingNotifier
  1299.                 try:
  1300.                     self.idlingNotifier.join()
  1301.                 except:
  1302.                     pass
  1303.                 self.idlingNotifier = None
  1304.             elif self.idlingNotifier is None:
  1305.                 self.idlingNotifier = idlenotifier.IdleNotifier(self)
  1306.                 self.idlingNotifier.start()
  1307.  
  1308.     ### Handling system idle events
  1309.     
  1310.     def systemHasBeenIdlingSince(self, seconds):
  1311.         self.setUpstreamLimit(False)
  1312.  
  1313.     def systemIsActiveAgain(self):
  1314.         self.setUpstreamLimit(True)
  1315.  
  1316.     ### Handling events received from the OS (via our base class) ###
  1317.  
  1318.     # Called by Frontend via Application base class in response to OS request.
  1319.     def addAndSelectFeed(self, url = None, showTemplate = None):
  1320.         return GUIActionHandler().addFeed(url, showTemplate)
  1321.  
  1322.     def addAndSelectGuide(self, url = None):
  1323.         return GUIActionHandler().addGuide(url)
  1324.  
  1325.     def addSearchFeed(self, term=None, style=dialogs.SearchChannelDialog.CHANNEL, location = None):
  1326.         return GUIActionHandler().addSearchFeed(term, style, location)
  1327.  
  1328.     def testSearchFeedDialog(self):
  1329.         return GUIActionHandler().testSearchFeedDialog()
  1330.  
  1331.     ### Handling 'DTVAPI' events from the channel guide ###
  1332.  
  1333.     def addFeed(self, url = None):
  1334.         return GUIActionHandler().addFeed(url, selected = None)
  1335.  
  1336.     def selectFeed(self, url):
  1337.         return GUIActionHandler().selectFeed(url)
  1338.  
  1339.     ### Keep track of currently available+downloading items and refresh the
  1340.     ### corresponding tabs accordingly.
  1341.  
  1342.     def onUnwatchedItemsCountChange(self, obj, id):
  1343.         assert self.newTab is not None
  1344.         self.newTab.redraw()
  1345.         self.updateAvailableItemsCountFeedback()
  1346.         if hasattr(frontend.Application, "onUnwatchedItemsCountChange"):
  1347.             frontend.Application.onUnwatchedItemsCountChange(self, obj, id)
  1348.  
  1349.     def onDownloadingItemsCountChange(self, obj, id):
  1350.         assert self.downloadTab is not None
  1351.         self.downloadTab.redraw()
  1352.         if hasattr(frontend.Application, "onDownloadingItemsCountChange"):
  1353.             frontend.Application.onDownloadingItemsCountChange(self, obj, id)
  1354.  
  1355.     def updateAvailableItemsCountFeedback(self):
  1356.         global delegate
  1357.         count = views.unwatchedItems.len()
  1358.         delegate.updateAvailableItemsCountFeedback(count)
  1359.  
  1360.     ### Chrome search:
  1361.     ### Switch to the search tab and perform a search using the specified engine.
  1362.  
  1363.     def performSearch(self, engine, query):
  1364.         util.checkU(engine)
  1365.         util.checkU(query)
  1366.         handler = TemplateActionHandler(None, None)
  1367.         handler.updateLastSearchEngine(engine)
  1368.         handler.updateLastSearchQuery(query)
  1369.         handler.performSearch(engine, query)
  1370.         self.selection.selectTabByTemplateBase('searchtab')
  1371.  
  1372.     ### ----
  1373.  
  1374.     def setUpstreamLimit(self, setLimit):
  1375.         if setLimit:
  1376.             limit = config.get(prefs.UPSTREAM_LIMIT_IN_KBS)
  1377.             # upstream limit should be set here
  1378.         else:
  1379.             # upstream limit should be unset here
  1380.             pass
  1381.  
  1382.     def handleURIDrop(self, data, **kwargs):
  1383.         """Handle an external drag that contains a text/uri-list mime-type.
  1384.         data should be the text/uri-list data, in escaped form.
  1385.  
  1386.         kwargs is thrown away.  It exists to catch weird URLs, like
  1387.         javascript: which sometime result in us getting extra arguments.
  1388.         """
  1389.  
  1390.         lastAddedFeed = None
  1391.         data = urllib.unquote(data)
  1392.         for url in data.split(u"\n"):
  1393.             url = url.strip()
  1394.             if url == u"":
  1395.                 continue
  1396.             if url.startswith(u"file://"):
  1397.                 filename = download_utils.getFileURLPath(url)
  1398.                 filename = platformutils.osFilenameToFilenameType(filename)
  1399.                 eventloop.addIdle (singleclick.openFile,
  1400.                     "Open Dropped file", args=(filename,))
  1401.             elif url.startswith(u"http:") or url.startswith(u"https:"):
  1402.                 url = feed.normalizeFeedURL(url)
  1403.                 if feed.validateFeedURL(url) and not feed.getFeedByURL(url):
  1404.                     lastAddedFeed = feed.Feed(url)
  1405.  
  1406.         if lastAddedFeed:
  1407.             controller.selection.selectTabByObject(lastAddedFeed)
  1408.  
  1409.     def handleDrop(self, dropData, type, sourceData):
  1410.         try:
  1411.             destType, destID = dropData.split("-")
  1412.             if destID == 'END':
  1413.                 destObj = None
  1414.             elif destID == 'START':
  1415.                 if destType == 'channel':
  1416.                     tabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  1417.                 else:
  1418.                     tabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  1419.                 for tab in tabOrder.getView():
  1420.                     destObj = tab.obj
  1421.                     break
  1422.             else:
  1423.                 destObj = db.getObjectByID(int(destID))
  1424.             sourceArea, sourceID = sourceData.split("-")
  1425.             sourceID = int(sourceID)
  1426.             draggedIDs = self.selection.calcSelection(sourceArea, sourceID)
  1427.         except:
  1428.             logging.exception ("error parsing drop (%r, %r, %r)",
  1429.                                dropData, type, sourceData)
  1430.             return
  1431.  
  1432.         if destType == 'playlist' and type == 'downloadeditem':
  1433.             # dropping an item on a playlist
  1434.             destObj.handleDNDAppend(draggedIDs)
  1435.         elif ((destType == 'channelfolder' and type == 'channel') or
  1436.                 (destType == 'playlistfolder' and type == 'playlist')):
  1437.             # Dropping a channel/playlist onto a folder
  1438.             obj = db.getObjectByID(int(destID))
  1439.             obj.handleDNDAppend(draggedIDs)
  1440.         elif (destType in ('playlist', 'playlistfolder') and 
  1441.                 type in ('playlist', 'playlistfolder')):
  1442.             # Reording the playlist tabs
  1443.             tabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  1444.             tabOrder.handleDNDReorder(destObj, draggedIDs)
  1445.         elif (destType in ('channel', 'channelfolder') and
  1446.                 type in ('channel', 'channelfolder')):
  1447.             # Reordering the channel tabs
  1448.             tabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  1449.             tabOrder.handleDNDReorder(destObj, draggedIDs)
  1450.         elif destType == "playlistitem" and type == "downloadeditem":
  1451.             # Reording items in a playlist
  1452.             playlist = self.selection.getSelectedTabs()[0].obj
  1453.             playlist.handleDNDReorder(destObj, draggedIDs)
  1454.         else:
  1455.             logging.info ("Can't handle drop. Dest type: %s Dest id: %s Type: %s",
  1456.                           destType, destID, type)
  1457.  
  1458.     def addToNewPlaylist(self):
  1459.         selected = controller.selection.getSelectedItems()
  1460.         childIDs = [i.getID() for i in selected if i.isDownloaded()]
  1461.         playlist.createNewPlaylist(childIDs)
  1462.  
  1463.     def startUploads(self):
  1464.         selected = controller.selection.getSelectedItems()
  1465.         for i in selected:
  1466.             i.startUpload()
  1467.  
  1468.     def newDownload(self, url = None):
  1469.         return GUIActionHandler().addDownload(url)
  1470.         
  1471.     def importChannels(self):
  1472.         importer = opml.Importer()
  1473.         importer.importSubscriptions()
  1474.     
  1475.     def exportChannels(self):
  1476.         exporter = opml.Exporter()
  1477.         exporter.exportSubscriptions()
  1478.  
  1479. ###############################################################################
  1480. #### TemplateDisplay: a HTML-template-driven right-hand display panel      ####
  1481. ###############################################################################
  1482.  
  1483. class TemplateDisplay(frontend.HTMLDisplay):
  1484.  
  1485.     def __init__(self, templateName, templateState, frameHint=None, areaHint=None, 
  1486.             baseURL=None, *args, **kargs):
  1487.         """'templateName' is the name of the inital template file.  'data' is
  1488.         keys for the template. 'templateState' is a string with the state of the
  1489.         template.
  1490.         """
  1491.  
  1492.         logging.debug ("Processing %s", templateName)
  1493.         self.templateName = templateName
  1494.         self.templateState = templateState
  1495.         (tch, self.templateHandle) = template.fillTemplate(templateName,
  1496.                 self, self.getDTVPlatformName(), self.getEventCookie(),
  1497.                 self.getBodyTagExtra(), templateState = templateState,
  1498.                                                            *args, **kargs)
  1499.         self.args = args
  1500.         self.kargs = kargs
  1501.         self.haveLoaded = False
  1502.         html = tch.read()
  1503.  
  1504.         self.actionHandlers = [
  1505.             ModelActionHandler(delegate),
  1506.             HistoryActionHandler(self),
  1507.             GUIActionHandler(),
  1508.             TemplateActionHandler(self, self.templateHandle),
  1509.             ]
  1510.  
  1511.         loadTriggers = self.templateHandle.getTriggerActionURLsOnLoad()
  1512.         newPage = self.runActionURLs(loadTriggers)
  1513.  
  1514.         if newPage:
  1515.             self.templateHandle.unlinkTemplate()
  1516.             # FIXME - url is undefined here!
  1517.             self.__init__(re.compile(r"^template:(.*)$").match(url).group(1), frameHint, areaHint, baseURL)
  1518.         else:
  1519.             frontend.HTMLDisplay.__init__(self, html, frameHint=frameHint, areaHint=areaHint, baseURL=baseURL)
  1520.  
  1521.             self.templateHandle.initialFillIn()
  1522.  
  1523.     def __eq__(self, other):
  1524.         return (other.__class__ == TemplateDisplay and 
  1525.                 self.templateName == other.templateName and 
  1526.                 self.args == other.args and 
  1527.                 self.kargs == other.kargs)
  1528.  
  1529.     def __str__(self):
  1530.         return "Template <%s> args=%s kargs=%s" % (self.templateName, self.args, self.kargs)
  1531.  
  1532.     def reInit(self, *args, **kargs):
  1533.         self.args = args
  1534.         self.kargs = kargs
  1535.         try:
  1536.             self.templateHandle.templateVars['reInit'](*args, **kargs)
  1537.         except:
  1538.             pass
  1539.         self.templateHandle.forceUpdate()
  1540.         
  1541.     def runActionURLs(self, triggers):
  1542.         newPage = False
  1543.         for url in triggers:
  1544.             if url.startswith('action:'):
  1545.                 self.onURLLoad(url)
  1546.             elif url.startswith('template:'):
  1547.                 newPage = True
  1548.                 break
  1549.         return newPage
  1550.  
  1551.     def parseEventURL(self, url):
  1552.         match = re.match(r"[a-zA-Z]+:([^?]+)(\?(.*))?$", url)
  1553.         if match:
  1554.             path = match.group(1)
  1555.             argString = match.group(3)
  1556.             if argString is None:
  1557.                 argString = u''
  1558.             argString = argString.encode('utf8')
  1559.             # argString is turned into a str since parse_qs will fail on utf8 that has been url encoded.
  1560.             argLists = cgi.parse_qs(argString, keep_blank_values=True)
  1561.  
  1562.             # argLists is a dictionary from parameter names to a list
  1563.             # of values given for that parameter. Take just one value
  1564.             # for each parameter, raising an error if more than one
  1565.             # was given.
  1566.             args = {}
  1567.             for key in argLists.keys():
  1568.                 value = argLists[key]
  1569.                 if len(value) != 1:
  1570.                     import template_compiler
  1571.                     raise template_compiler.TemplateError, "Multiple values of '%s' argument passed to '%s' action" % (key, url)
  1572.                 # Cast the value results back to unicode
  1573.                 try:
  1574.                     args[key.encode('ascii','replace')] = value[0].decode('utf8')
  1575.                 except:
  1576.                     args[key.encode('ascii','replace')] = value[0].decode('ascii', 'replace')
  1577.             return path, args
  1578.         else:
  1579.             raise ValueError("Badly formed eventURL: %s" % url)
  1580.  
  1581.  
  1582.     def onURLLoad(self, url):
  1583.         if self.checkURL(url):
  1584.             if not controller.guide: # not on a channel guide:
  1585.                 return True
  1586.             # The first time the guide is loaded in the template, several
  1587.             # pages are loaded, so this shouldn't be called during that
  1588.             # first load.  After that, this shows the spinning circle to
  1589.             # indicate loading
  1590.             if not self.haveLoaded and (url ==
  1591.                     controller.guide.getLastVisitedURL()):
  1592.                 self.haveLoaded = True
  1593.             elif self.haveLoaded:
  1594.                 script = 'top.miro_navigation_frame.guideUnloaded()'
  1595.                 if not url.endswith(script):
  1596.                     self.execJS('top.miro_navigation_frame.guideUnloaded()')
  1597.             return True
  1598.         else:
  1599.             return False
  1600.  
  1601.     # Returns true if the browser should handle the URL.
  1602.     def checkURL(self, url):
  1603.         util.checkU(url)
  1604.         logging.info ("got %s", url)
  1605.         try:
  1606.             # Special-case non-'action:'-format URL
  1607.             if url.startswith (u"template:"):
  1608.                 name, args = self.parseEventURL(url)
  1609.                 self.dispatchAction('switchTemplate', name=name, **args)
  1610.                 return False
  1611.  
  1612.             # Standard 'action:' URL
  1613.             if url.startswith (u"action:"):
  1614.                 action, args = self.parseEventURL(url)
  1615.                 self.dispatchAction(action, **args)
  1616.                 return False
  1617.  
  1618.             # Let channel guide URLs pass through
  1619.             if controller.guide is not None and \
  1620.                    controller.guide.isPartOfGuide(url):
  1621.                 controller.setLastVisitedGuideURL(url)
  1622.                 return True
  1623.             if url.startswith(u'file://'):
  1624.                 path = download_utils.getFileURLPath(url)
  1625.                 return os.path.exists(path)
  1626.  
  1627.             # If we get here, this isn't a DTV URL. We should open it
  1628.             # in an external browser.
  1629.             if (url.startswith(u'http://') or url.startswith(u'https://') or
  1630.                 url.startswith(u'ftp://') or url.startswith(u'mailto:') or
  1631.                 url.startswith(u'feed://')):
  1632.                 self.handleCandidateExternalURL(url)
  1633.                 return False
  1634.  
  1635.         except:
  1636.             details = "Handling action URL '%s'" % (url, )
  1637.             util.failedExn("while handling a request", details = details)
  1638.  
  1639.         return True
  1640.  
  1641.     @eventloop.asUrgent
  1642.     def handleCandidateExternalURL(self, url):
  1643.         """Open a URL that onURLLoad thinks is an external URL.
  1644.         handleCandidateExternalURL does extra checks that onURLLoad can't do
  1645.         because it's happens in the gui thread and can't access the DB.
  1646.         """
  1647.  
  1648.         # check for subscribe.getdemocracy.com links
  1649.         type, subscribeURLs = subscription.findSubscribeLinks(url)
  1650.  
  1651.         # check if the url that came from a guide, but the user switched tabs
  1652.         # before it went through.
  1653.         if len(subscribeURLs) == 0:
  1654.             for guideObj in views.guides:
  1655.                 if guideObj.isPartOfGuide(url):
  1656.                     return
  1657.  
  1658.         normalizedURLs = []
  1659.         for url in subscribeURLs:
  1660.             normalized = feed.normalizeFeedURL(url)
  1661.             if feed.validateFeedURL(normalized):
  1662.                 normalizedURLs.append(normalized)
  1663.         if normalizedURLs:
  1664.             if type == 'feed':
  1665.                 for url in normalizedURLs:
  1666.                     if feed.getFeedByURL(url) is None:
  1667.                         newFeed = feed.Feed(url)
  1668.                         newFeed.blink()
  1669.             elif type == 'download':
  1670.                 for url in normalizedURLs:
  1671.                     filename = platformutils.unicodeToFilename(url)
  1672.                     singleclick.downloadURL(filename)
  1673.             elif type == 'guide':
  1674.                 for url in normalizedURLs:
  1675.                     if guide.getGuideByURL (url) is None:
  1676.                         guide.ChannelGuide(url)
  1677.             else:
  1678.                 raise AssertionError("Unkown subscribe type")
  1679.             return
  1680.  
  1681.         if url.startswith(u'feed://'):
  1682.             url = u"http://" + url[len(u"feed://"):]
  1683.             f = feed.getFeedByURL(url)
  1684.             if f is None:
  1685.                 f = feed.Feed(url)
  1686.             f.blink()
  1687.             return
  1688.  
  1689.         delegate.openExternalURL(url)
  1690.  
  1691.     @eventloop.asUrgent
  1692.     def dispatchAction(self, action, **kwargs):
  1693.         called = False
  1694.         start = clock()
  1695.         for handler in self.actionHandlers:
  1696.             if hasattr(handler, action):
  1697.                 getattr(handler, action)(**kwargs)
  1698.                 called = True
  1699.                 break
  1700.         end = clock()
  1701.         if end - start > 0.5:
  1702.             logging.timing ("dispatch action %s too slow (%.3f secs)", action, end - start)
  1703.         if not called:
  1704.             logging.warning ("Ignored bad action URL: action=%s", action)
  1705.  
  1706.     @eventloop.asUrgent
  1707.     def onDeselected(self, frame):
  1708.         unloadTriggers = self.templateHandle.getTriggerActionURLsOnUnload()
  1709.         self.runActionURLs(unloadTriggers)
  1710.         self.unlink()
  1711.         frontend.HTMLDisplay.onDeselected(self, frame)
  1712.  
  1713.     def unlink(self):
  1714.         self.templateHandle.unlinkTemplate()
  1715.         self.actionHandlers = []
  1716.  
  1717. ###############################################################################
  1718. #### Handlers for actions generated from templates, the OS, etc            ####
  1719. ###############################################################################
  1720.  
  1721. # Functions that are safe to call from action: URLs that do nothing
  1722. # but manipulate the database.
  1723. class ModelActionHandler:
  1724.     
  1725.     def __init__(self, backEndDelegate):
  1726.         self.backEndDelegate = backEndDelegate
  1727.     
  1728.     def setAutoDownloadMode(self, feed, mode):
  1729.         obj = db.getObjectByID(int(feed))
  1730.         obj.setAutoDownloadMode(mode)
  1731.  
  1732.     def setExpiration(self, feed, type, time):
  1733.         obj = db.getObjectByID(int(feed))
  1734.         obj.setExpiration(type, int(time))
  1735.  
  1736.     def setMaxNew(self, feed, maxNew):
  1737.         obj = db.getObjectByID(int(feed))
  1738.         obj.setMaxNew(int(maxNew))
  1739.  
  1740.     def invalidMaxNew(self, value):
  1741.         title = _("Invalid Value")
  1742.         description = _("%s is invalid.  You must enter a non-negative "
  1743.                 "number.") % value
  1744.         dialogs.MessageBoxDialog(title, description).run()
  1745.  
  1746.     def startDownload(self, item):
  1747.         try:
  1748.             obj = db.getObjectByID(int(item))
  1749.             obj.download()
  1750.         except database.ObjectNotFoundError:
  1751.             pass
  1752.  
  1753.     def removeFeed(self, id):
  1754.         try:
  1755.             feed = db.getObjectByID(int(id))
  1756.             controller.removeFeed(feed)
  1757.         except database.ObjectNotFoundError:
  1758.             pass
  1759.  
  1760.     def removeCurrentFeed(self):
  1761.         controller.removeCurrentFeed()
  1762.  
  1763.     def removeCurrentPlaylist(self):
  1764.         controller.removeCurrentPlaylist()
  1765.  
  1766.     def removeCurrentItems(self):
  1767.         controller.removeCurrentItems()
  1768.  
  1769.     def mergeToFolder(self):
  1770.         tls = controller.selection.tabListSelection
  1771.         selectionType = tls.getType()
  1772.         childIDs = set(tls.currentSelection)
  1773.         if selectionType == 'channeltab':
  1774.             folder.createNewChannelFolder(childIDs)
  1775.         elif selectionType == 'playlisttab':
  1776.             folder.createNewPlaylistFolder(childIDs)
  1777.         else:
  1778.             logging.warning ("bad selection type %s in mergeToFolder",
  1779.                              selectionType)
  1780.  
  1781.     def remove(self, area, id):
  1782.         selectedIDs = controller.selection.calcSelection(area, int(id))
  1783.         selectedObjects = [db.getObjectByID(id) for id in selectedIDs]
  1784.         objType = selectedObjects[0].__class__
  1785.  
  1786.         if objType in (feed.Feed, folder.ChannelFolder):
  1787.             controller.removeFeeds(selectedObjects)
  1788.         elif objType in (playlist.SavedPlaylist, folder.PlaylistFolder):
  1789.             controller.removePlaylists(selectedObjects)
  1790.         elif objType == guide.ChannelGuide:
  1791.             if len(selectedObjects) != 1:
  1792.                 raise AssertionError("Multiple guides selected in remove")
  1793.             controller.removeGuide(selectedObjects[0])
  1794.         elif objType == item.Item:
  1795.             pl = controller.selection.getSelectedTabs()[0].obj
  1796.             pl.handleRemove(destObj, selectedIDs)
  1797.         else:
  1798.             logging.warning ("Can't handle type %s in remove()", objType)
  1799.  
  1800.     def rename(self, id):
  1801.         try:
  1802.             obj = db.getObjectByID(int(id))
  1803.         except:
  1804.             logging.warning ("tried to rename object that doesn't exist with id %d", int(feed))
  1805.             return
  1806.         if obj.__class__ in (playlist.SavedPlaylist, folder.ChannelFolder,
  1807.                 folder.PlaylistFolder):
  1808.             obj.rename()
  1809.         else:
  1810.             logging.warning ("Unknown object type in remove() %s", type(obj))
  1811.  
  1812.     def updateFeed(self, feed):
  1813.         obj = db.getObjectByID(int(feed))
  1814.         obj.update()
  1815.  
  1816.     def copyFeedURL(self, feed):
  1817.         obj = db.getObjectByID(int(feed))
  1818.         url = obj.getURL()
  1819.         self.backEndDelegate.copyTextToClipboard(url)
  1820.  
  1821.     def markFeedViewed(self, feed):
  1822.         try:
  1823.             obj = db.getObjectByID(int(feed))
  1824.             obj.markAsViewed()
  1825.         except database.ObjectNotFoundError:
  1826.             pass
  1827.  
  1828.     def updateIcons(self, feed):
  1829.         try:
  1830.             obj = db.getObjectByID(int(feed))
  1831.             obj.updateIcons()
  1832.         except database.ObjectNotFoundError:
  1833.             pass
  1834.  
  1835.     def expireItem(self, item):
  1836.         try:
  1837.             obj = db.getObjectByID(int(item))
  1838.             obj.expire()
  1839.         except database.ObjectNotFoundError:
  1840.             logging.warning ("tried to expire item that doesn't exist with id %d", int(item))
  1841.  
  1842.     def expirePlayingItem(self, item):
  1843.         self.expireItem(item)
  1844.         controller.playbackController.skip(1)
  1845.  
  1846.     def addItemToLibrary(self, item):
  1847.         obj = db.getObjectByID(int(item))
  1848.         manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1849.         obj.setFeed(manualFeed.getID())
  1850.  
  1851.     def keepItem(self, item):
  1852.         obj = db.getObjectByID(int(item))
  1853.         obj.save()
  1854.  
  1855.     def stopUploadItem(self, item):
  1856.         obj = db.getObjectByID(int(item))
  1857.         obj.stopUpload()
  1858.  
  1859.     def toggleMoreItemInfo(self, item):
  1860.         obj = db.getObjectByID(int(item))
  1861.         obj.toggleShowMoreInfo()
  1862.  
  1863.     def revealItem(self, item):
  1864.         obj = db.getObjectByID(int(item))
  1865.         filename = obj.getFilename()
  1866.         if not os.path.exists(filename):
  1867.             basename = os.path.basename(filename)
  1868.             title = _("Error Revealing File")
  1869.             msg = _("The file \"%s\" was deleted from outside Miro.") % basename
  1870.             dialogs.MessageBoxDialog(title, msg).run()
  1871.         else:
  1872.             self.backEndDelegate.revealFile(filename)
  1873.  
  1874.     def clearTorrents (self):
  1875.         items = views.items.filter(lambda x: x.getFeed().url == u'dtv:manualFeed' \
  1876.                                              and x.isNonVideoFile() \
  1877.                                              and not x.getState() == u"paused" \
  1878.                                              and not x.getState() == u"downloading")
  1879.         for i in items:
  1880.             if i.downloader is not None:
  1881.                 i.downloader.setDeleteFiles(False)
  1882.             i.remove()
  1883.  
  1884.     def pauseDownload(self, item):
  1885.         obj = db.getObjectByID(int(item))
  1886.         obj.pause()
  1887.         
  1888.     def resumeDownload(self, item):
  1889.         obj = db.getObjectByID(int(item))
  1890.         obj.resume()
  1891.  
  1892.     def pauseAll (self):
  1893.         autodler.pauseDownloader()
  1894.         for item in views.downloadingItems:
  1895.             item.pause()
  1896.  
  1897.     def resumeAll (self):
  1898.         for item in views.pausedItems:
  1899.             item.resume()
  1900.         autodler.resumeDownloader()
  1901.  
  1902.     def toggleExpand(self, id):
  1903.         obj = db.getObjectByID(int(id))
  1904.         obj.setExpanded(not obj.getExpanded())
  1905.  
  1906.     def setRunAtStartup(self, value):
  1907.         value = (value == "1")
  1908.         self.backEndDelegate.setRunAtStartup(value)
  1909.  
  1910.     def setCheckEvery(self, value):
  1911.         value = int(value)
  1912.         config.set(prefs.CHECK_CHANNELS_EVERY_X_MN,value)
  1913.  
  1914.     def setLimitUpstream(self, value):
  1915.         value = (value == "1")
  1916.         config.set(prefs.LIMIT_UPSTREAM,value)
  1917.  
  1918.     def setMaxUpstream(self, value):
  1919.         value = int(value)
  1920.         config.set(prefs.UPSTREAM_LIMIT_IN_KBS,value)
  1921.  
  1922.     def setPreserveDiskSpace(self, value):
  1923.         value = (value == "1")
  1924.         config.set(prefs.PRESERVE_DISK_SPACE,value)
  1925.  
  1926.     def setDefaultExpiration(self, value):
  1927.         value = int(value)
  1928.         config.set(prefs.EXPIRE_AFTER_X_DAYS,value)
  1929.  
  1930.     def videoBombExternally(self, item):
  1931.         obj = db.getObjectByID(int(item))
  1932.         paramList = {}
  1933.         paramList["title"] = obj.getTitle()
  1934.         paramList["info_url"] = obj.getLink()
  1935.         paramList["hookup_url"] = obj.getPaymentLink()
  1936.         try:
  1937.             rss_url = obj.getFeed().getURL()
  1938.             if (not rss_url.startswith(u'dtv:')):
  1939.                 paramList["rss_url"] = rss_url
  1940.         except:
  1941.             pass
  1942.         thumb_url = obj.getThumbnailURL()
  1943.         if thumb_url is not None:
  1944.             paramList["thumb_url"] = thumb_url
  1945.  
  1946.         # FIXME: add "explicit" and "tags" parameters when we get them in item
  1947.  
  1948.         paramString = ""
  1949.         glue = '?'
  1950.        
  1951.         # This should be first, since it's most important.
  1952.         url = obj.getURL()
  1953.         url.encode('utf-8', 'replace')
  1954.         if (not url.startswith('file:')):
  1955.             paramString = "?url=%s" % xhtmltools.urlencode(url)
  1956.             glue = '&'
  1957.  
  1958.         for key in paramList.keys():
  1959.             if len(paramList[key]) > 0:
  1960.                 paramString = "%s%s%s=%s" % (paramString, glue, key, xhtmltools.urlencode(paramList[key]))
  1961.                 glue = '&'
  1962.  
  1963.         # This should be last, so that if it's extra long it 
  1964.         # cut off all the other parameters
  1965.         description = obj.getDescription()
  1966.         if len(description) > 0:
  1967.             paramString = "%s%sdescription=%s" % (paramString, glue,
  1968.                     xhtmltools.urlencode(description))
  1969.         url = config.get(prefs.VIDEOBOMB_URL) + paramString
  1970.         self.backEndDelegate.openExternalURL(url)
  1971.  
  1972.     def changeMoviesDirectory(self, newDir, migrate):
  1973.         changeMoviesDirectory(newDir, migrate == '1')
  1974.  
  1975. # Test shim for test* functions on GUIActionHandler
  1976. class printResultThread(threading.Thread):
  1977.  
  1978.     def __init__(self, format, func):
  1979.         self.format = format
  1980.         self.func = func
  1981.         threading.Thread.__init__(self)
  1982.  
  1983.     def run(self):
  1984.         print (self.format % (self.func(), ))
  1985.  
  1986. # Functions that change the history of a guide
  1987. class HistoryActionHandler:
  1988.  
  1989.     def __init__(self, display):
  1990.         self.display = display
  1991.  
  1992.     def gotoURL(self, newURL):
  1993.         self.display.execJS('top.miro_guide_frame.location="%s"' % newURL)
  1994.  
  1995.     def getGuide(self):
  1996.         guides = [t.obj for t in controller.selection.getSelectedTabs()]
  1997.         if len(guides) != 1:
  1998.             return
  1999.         if not isinstance(guides[0], guide.ChannelGuide):
  2000.             return
  2001.         return guides[0]
  2002.  
  2003.     def back(self):
  2004.         guide = self.getGuide()
  2005.         if guide is not None:
  2006.             newURL = guide.getHistoryURL(-1)
  2007.             if newURL is not None:
  2008.                 self.gotoURL(newURL)
  2009.  
  2010.     def forward(self):
  2011.         guide = self.getGuide()
  2012.         if guide is not None:
  2013.             newURL = guide.getHistoryURL(1)
  2014.             if newURL is not None:
  2015.                 self.gotoURL(newURL)
  2016.  
  2017.     def home(self):
  2018.         guide = self.getGuide()
  2019.         if guide is not None:
  2020.             newURL = guide.getHistoryURL(None)
  2021.             self.gotoURL(newURL)
  2022.  
  2023. # Functions that are safe to call from action: URLs that can change
  2024. # the GUI presentation (and may or may not manipulate the database.)
  2025. class GUIActionHandler:
  2026.  
  2027.     def playUnwatched(self):
  2028.         controller.playView(views.unwatchedItems)
  2029.  
  2030.     def openFile(self, path):
  2031.         singleclick.openFile(path)
  2032.  
  2033.     def addSearchFeed(self, term=None, style = dialogs.SearchChannelDialog.CHANNEL, location = None):
  2034.         def doAdd(dialog):
  2035.             if dialog.choice == dialogs.BUTTON_CREATE_CHANNEL:
  2036.                 self.addFeed(dialog.getURL())
  2037.         dialog = dialogs.SearchChannelDialog(term, style, location)
  2038.         if location == None:
  2039.             dialog.run(doAdd)
  2040.         else:
  2041.             self.addFeed(dialog.getURL())
  2042.  
  2043.     def addChannelSearchFeed(self, id):
  2044.         feed = db.getObjectByID(int(id))
  2045.         self.addSearchFeed(feed.inlineSearchTerm, dialogs.SearchChannelDialog.CHANNEL, int(id))
  2046.  
  2047.     def addEngineSearchFeed(self, term, name):
  2048.         self.addSearchFeed(term, dialogs.SearchChannelDialog.ENGINE, name)
  2049.         
  2050.     def testSearchFeedDialog(self):
  2051.         def finish(dialog):
  2052.             pass
  2053.         def thirdDialog(dialog):
  2054.             dialog = dialogs.SearchChannelDialog("Should select URL http://testurl/", dialogs.SearchChannelDialog.URL, "http://testurl/")
  2055.             dialog.run(finish)
  2056.         def secondDialog(dialog):
  2057.             dialog = dialogs.SearchChannelDialog("Should select YouTube engine", dialogs.SearchChannelDialog.ENGINE, "youtube")
  2058.             dialog.run(thirdDialog)
  2059.         dialog = dialogs.SearchChannelDialog("Should select third channel in list", dialogs.SearchChannelDialog.CHANNEL, -1)
  2060.         dialog.run(secondDialog)
  2061.         
  2062.     def addURL(self, title, message, callback, url = None):
  2063.         util.checkU(url)
  2064.         util.checkU(title)
  2065.         util.checkU(message)
  2066.         def createDialog(ltitle, lmessage, prefill = None):
  2067.             def prefillCallback():
  2068.                 if prefill:
  2069.                     return prefill
  2070.                 else:
  2071.                     return None
  2072.             dialog = dialogs.TextEntryDialog(ltitle, lmessage, dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL, prefillCallback, fillWithClipboardURL=(prefill is None))
  2073.             def callback(dialog):
  2074.                 if dialog.choice == dialogs.BUTTON_OK:
  2075.                     doAdd(dialog.value)
  2076.             dialog.run(callback)
  2077.         def doAdd(url):
  2078.             normalizedURL = feed.normalizeFeedURL(url)
  2079.             if not feed.validateFeedURL(normalizedURL):
  2080.                 ltitle = title + _(" - Invalid URL")
  2081.                 lmessage = _("The address you entered is not a valid URL.\nPlease double check and try again.\n\n") + message
  2082.                 createDialog(ltitle, lmessage, url)
  2083.                 return
  2084.             callback(normalizedURL)
  2085.         if url is None:
  2086.             createDialog(title, message)
  2087.         else:
  2088.             doAdd(url)
  2089.         
  2090.     # NEEDS: name should change to addAndSelectFeed; then we should create
  2091.     # a non-GUI addFeed to match removeFeed. (requires template updates)
  2092.     def addFeed(self, url = None, showTemplate = None, selected = '1'):
  2093.         if url:
  2094.             util.checkU(url)
  2095.         def doAdd (url):
  2096.             db.confirmDBThread()
  2097.             myFeed = feed.getFeedByURL (url)
  2098.             if myFeed is None:
  2099.                 myFeed = feed.Feed(url)
  2100.     
  2101.             if selected == '1':
  2102.                 controller.selection.selectTabByObject(myFeed)
  2103.             else:
  2104.                 myFeed.blink()
  2105.         self.addURL (Template(_("$shortAppName - Add Channel")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the channel to add"), doAdd, url)
  2106.  
  2107.     def selectFeed(self, url):
  2108.         url = feed.normalizeFeedURL(url)
  2109.         db.confirmDBThread()
  2110.         # Find the feed
  2111.         myFeed = feed.getFeedByURL (url)
  2112.         if myFeed is None:
  2113.             logging.warning ("selectFeed: no such feed: %s", url)
  2114.             return
  2115.         controller.selection.selectTabByObject(myFeed)
  2116.         
  2117.     def addGuide(self, url = None, selected = '1'):
  2118.         def doAdd(url):
  2119.             db.confirmDBThread()
  2120.             myGuide = guide.getGuideByURL (url)
  2121.             if myGuide is None:
  2122.                 myGuide = guide.ChannelGuide(url)
  2123.     
  2124.             if selected == '1':
  2125.                 controller.selection.selectTabByObject(myGuide)
  2126.         self.addURL (Template(_("$shortAppName - Add Miro Guide")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the Miro Guide to add"), doAdd, url)
  2127.  
  2128.     def addDownload(self, url = None):
  2129.         def doAdd(url):
  2130.             db.confirmDBThread()
  2131.             singleclick.downloadURL(platformutils.unicodeToFilename(url))
  2132.         self.addURL (Template(_("$shortAppName - Download Video")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the video to download"), doAdd, url)
  2133.  
  2134.     def handleDrop(self, data, type, sourcedata):
  2135.         controller.handleDrop(data, type, sourcedata)
  2136.  
  2137.     def handleURIDrop(self, data, **kwargs):
  2138.         controller.handleURIDrop(data, **kwargs)
  2139.  
  2140.     def showHelp(self):
  2141.         delegate.openExternalURL(config.get(prefs.HELP_URL))
  2142.  
  2143.     def reportBug(self):
  2144.         delegate.openExternalURL(config.get(prefs.BUG_REPORT_URL))
  2145.  
  2146. # Functions that are safe to call from action: URLs that change state
  2147. # specific to a particular instantiation of a template, and so have to
  2148. # be scoped to a particular HTML display widget.
  2149. class TemplateActionHandler:
  2150.     
  2151.     def __init__(self, display, templateHandle):
  2152.         self.display = display
  2153.         self.templateHandle = templateHandle
  2154.         self.currentName = None
  2155.  
  2156.     def switchTemplate(self, name, state='default', baseURL=None, *args, **kargs):
  2157.         self.templateHandle.unlinkTemplate()
  2158.         # Switch to new template. It get the same variable
  2159.         # dictionary as we have.
  2160.         # NEEDS: currently we hardcode the display area. This means
  2161.         # that these links always affect the right-hand 'content'
  2162.         # area, even if they are loaded from the left-hand 'tab'
  2163.         # area. Actually this whole invocation is pretty hacky.
  2164.         template = TemplateDisplay(name, state, frameHint=controller.frame,
  2165.                 areaHint=controller.frame.mainDisplay, baseURL=baseURL,
  2166.                 *args, **kargs)
  2167.         controller.frame.selectDisplay(template, controller.frame.mainDisplay)
  2168.         self.currentName = name
  2169.  
  2170.     def setViewFilter(self, viewName, fieldKey, functionKey, parameter, invert):
  2171.         logging.warning ("setViewFilter deprecated")
  2172.  
  2173.     def setViewSort(self, viewName, fieldKey, functionKey, reverse="false"):
  2174.         logging.warning ("setViewSort deprecated")
  2175.  
  2176.     def setSearchString(self, searchString):
  2177.         try:
  2178.             self.templateHandle.getTemplateVariable('updateSearchString')(unicode(searchString))
  2179.         except KeyError, e:
  2180.             logging.warning ("KeyError in getTemplateVariable ('updateSearchString')")
  2181.  
  2182.     def toggleDownloadsView(self):
  2183.         try:
  2184.             self.templateHandle.getTemplateVariable('toggleDownloadsView')(self.templateHandle)
  2185.         except KeyError, e:
  2186.             logging.warning ("KeyError in getTemplateVariable ('toggleDownloadsView')")
  2187.  
  2188.     def toggleWatchableView(self):
  2189.         try:
  2190.             self.templateHandle.getTemplateVariable('toggleWatchableView')(self.templateHandle)
  2191.         except KeyError, e:
  2192.             logging.warning ("KeyError in getTemplateVariable ('toggleWatchableView')")
  2193.  
  2194.     def toggleNewItemsView(self):
  2195.         try:
  2196.             self.templateHandle.getTemplateVariable('toggleNewItemsView')(self.templateHandle)
  2197.         except KeyError, e:
  2198.             logging.warning ("KeyError in getTemplateVariable ('toggleNewItemsView')")            
  2199.  
  2200.     def toggleAllItemsMode(self):
  2201.         try:
  2202.             self.templateHandle.getTemplateVariable('toggleAllItemsMode')(self.templateHandle)
  2203.         except KeyError, e:
  2204.             logging.warning ("KeyError in getTemplateVariable ('toggleAllItemsMode')")
  2205.  
  2206.     def pauseDownloads(self):
  2207.         try:
  2208.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2209.         except KeyError, e:
  2210.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during pauseDownloads()")
  2211.             return
  2212.         for item in view:
  2213.             item.pause()
  2214.  
  2215.     def resumeDownloads(self):
  2216.         try:
  2217.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2218.         except KeyError, e:
  2219.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during resumeDownloads()")
  2220.             return
  2221.         for item in view:
  2222.             item.resume()
  2223.  
  2224.     def cancelDownloads(self):
  2225.         try:
  2226.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2227.         except KeyError, e:
  2228.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during cancelDownloads()")
  2229.             return
  2230.         for item in view:
  2231.             item.expire()
  2232.  
  2233.     def playViewNamed(self, viewName, firstItemId):
  2234.         try:
  2235.             view = self.templateHandle.getTemplateVariable(viewName)
  2236.         except KeyError, e:
  2237.             logging.warning ("KeyError in getTemplateVariable (%s) during playViewNamed()" % (viewName,))
  2238.             return
  2239.         controller.playView(view, firstItemId)
  2240.  
  2241.     def playOneItem(self, viewName, itemID):
  2242.         try:
  2243.             view = self.templateHandle.getTemplateVariable(viewName)
  2244.         except KeyError, e:
  2245.             logging.warning ("KeyError in getTemplateVariable (%s) during playOneItem()" % (viewName,))
  2246.             return
  2247.         controller.playView(view, itemID, justPlayOne=True)
  2248.  
  2249.     def playNewVideos(self, id):
  2250.         try:
  2251.             obj = db.getObjectByID(int(id))
  2252.         except database.ObjectNotFoundError:
  2253.             return
  2254.  
  2255.         def myUnwatchedItems(obj):
  2256.             return (obj.getState() == u'newly-downloaded' and
  2257.                     not obj.isNonVideoFile() and
  2258.                     not obj.isContainerItem)
  2259.  
  2260.         controller.selection.selectTabByObject(obj, displayTabContent=False)
  2261.         if isinstance(obj, feed.Feed):
  2262.             feedView = views.items.filterWithIndex(indexes.itemsByFeed,
  2263.                     obj.getID())
  2264.             view = feedView.filter(myUnwatchedItems,
  2265.                                    sortFunc=sorts.item)
  2266.             controller.playView(view)
  2267.             view.unlink()
  2268.         elif isinstance(obj, folder.ChannelFolder):
  2269.             folderView = views.items.filterWithIndex(
  2270.                     indexes.itemsByChannelFolder, obj)
  2271.             view = folderView.filter(myUnwatchedItems,
  2272.                                      sortFunc=sorts.item)
  2273.             controller.playView(view)
  2274.             view.unlink()
  2275.         elif isinstance(obj, tabs.StaticTab): # new videos tab
  2276.             view = views.unwatchedItems
  2277.             controller.playView(view)
  2278.         else:
  2279.             raise TypeError("Can't get new videos for %s (type: %s)" % 
  2280.                     (obj, type(obj)))
  2281.  
  2282.     def playItemExternally(self, itemID):
  2283.         controller.playbackController.playItemExternally(itemID)
  2284.         
  2285.     def skipItem(self, itemID):
  2286.         controller.playbackController.skip(1)
  2287.     
  2288.     def updateLastSearchEngine(self, engine):
  2289.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2290.         if searchFeed is not None:
  2291.             searchFeed.lastEngine = engine
  2292.     
  2293.     def updateLastSearchQuery(self, query):
  2294.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2295.         if searchFeed is not None:
  2296.             searchFeed.lastQuery = query
  2297.         
  2298.     def performSearch(self, engine, query):
  2299.         util.checkU(engine)
  2300.         util.checkU(query)
  2301.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2302.         if searchFeed is not None and searchDownloadsFeed is not None:
  2303.             searchFeed.preserveDownloads(searchDownloadsFeed)
  2304.             searchFeed.lookup(engine, query)
  2305.  
  2306.     def resetSearch(self):
  2307.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2308.         if searchFeed is not None and searchDownloadsFeed is not None:
  2309.             searchFeed.preserveDownloads(searchDownloadsFeed)
  2310.             searchFeed.reset()
  2311.  
  2312.     def sortBy(self, by, section):
  2313.         try:
  2314.             self.templateHandle.getTemplateVariable('setSortBy')(by, section, self.templateHandle)
  2315.         except KeyError, e:
  2316.             logging.warning ("KeyError in getTemplateVariable ('setSortBy')")
  2317.  
  2318.     def handleSelect(self, area, viewName, id, shiftDown, ctrlDown):
  2319.         try:
  2320.             view = self.templateHandle.getTemplateVariable(viewName)
  2321.         except KeyError, e: # user switched templates before we got this
  2322.             logging.warning ("KeyError in getTemplateVariable (%s) during handleSelect()" % (viewName,))
  2323.             return
  2324.         shift = (shiftDown == '1')
  2325.         ctrl = (ctrlDown == '1')
  2326.         controller.selection.selectItem(area, view, int(id), shift, ctrl)
  2327.  
  2328.     def handleContextMenuSelect(self, id, area, viewName):
  2329.         try:
  2330.             obj = db.getObjectByID(int(id))
  2331.         except:
  2332.             traceback.print_exc()
  2333.         else:
  2334.             try:
  2335.                 view = self.templateHandle.getTemplateVariable(viewName)
  2336.             except KeyError, e: # user switched templates before we got this
  2337.                 logging.warning ("KeyError in getTemplateVariable (%s) during handleContextMenuSelect()" % (viewName,))
  2338.                 return
  2339.             if not controller.selection.isSelected(area, view, int(id)):
  2340.                 self.handleSelect(area, viewName, id, False, False)
  2341.             popup = menu.makeContextMenu(self.currentName, view,
  2342.                     controller.selection.getSelectionForArea(area), int(id))
  2343.             if popup:
  2344.                 delegate.showContextMenu(popup)
  2345.  
  2346.     def __getSearchFeeds(self):
  2347.         searchFeed = controller.getGlobalFeed('dtv:search')
  2348.         assert searchFeed is not None
  2349.         
  2350.         searchDownloadsFeed = controller.getGlobalFeed('dtv:searchDownloads')
  2351.         assert searchDownloadsFeed is not None
  2352.  
  2353.         return (searchFeed, searchDownloadsFeed)
  2354.  
  2355.     # The Windows XUL port can send a setVolume or setVideoProgress at
  2356.     # any time, even when there's no video display around. We can just
  2357.     # ignore it
  2358.     def setVolume(self, level):
  2359.         pass
  2360.     def setVideoProgress(self, pos):
  2361.         pass
  2362.  
  2363. # Helper: liberally interpret the provided string as a boolean
  2364. def stringToBoolean(string):
  2365.     if string == "" or string == "0" or string == "false":
  2366.         return False
  2367.     else:
  2368.         return True
  2369.  
  2370. ###############################################################################
  2371. #### Playlist & Video clips                                                ####
  2372. ###############################################################################
  2373.  
  2374. class Playlist:
  2375.     
  2376.     def __init__(self, view, firstItemId):
  2377.         self.initialView = view
  2378.         self.filteredView = self.initialView.filter(mappableToPlaylistItem)
  2379.         self.view = self.filteredView.map(mapToPlaylistItem)
  2380.  
  2381.         # Move the cursor to the requested item; if there's no
  2382.         # such item in the view, move the cursor to the first
  2383.         # item
  2384.         self.view.confirmDBThread()
  2385.         self.view.resetCursor()
  2386.         while True:
  2387.             cur = self.view.getNext()
  2388.             if cur == None:
  2389.                 # Item not found in view. Put cursor at the first
  2390.                 # item, if any.
  2391.                 self.view.resetCursor()
  2392.                 self.view.getNext()
  2393.                 break
  2394.             if firstItemId is None or cur.getID() == int(firstItemId):
  2395.                 # The cursor is now on the requested item.
  2396.                 break
  2397.  
  2398.     def reset(self):
  2399.         self.initialView.removeView(self.filteredView)
  2400.         self.initialView = None
  2401.         self.filteredView = None
  2402.         self.view = None
  2403.  
  2404.     def cur(self):
  2405.         return self.itemMarkedAsViewed(self.view.cur())
  2406.  
  2407.     def getNext(self):
  2408.         return self.itemMarkedAsViewed(self.view.getNext())
  2409.         
  2410.     def getPrev(self):
  2411.         return self.itemMarkedAsViewed(self.view.getPrev())
  2412.  
  2413.     def itemMarkedAsViewed(self, anItem):
  2414.         if anItem is not None:
  2415.             eventloop.addIdle(anItem.onViewed, "Mark item viewed")
  2416.         return anItem
  2417.  
  2418. class PlaylistItemFromItem:
  2419.  
  2420.     def __init__(self, anItem):
  2421.         self.item = anItem
  2422.         self.dcOnViewed = None
  2423.  
  2424.     def getTitle(self):
  2425.         return self.item.getTitle()
  2426.  
  2427.     def getVideoFilename(self):
  2428.         return self.item.getVideoFilename()
  2429.  
  2430.     def getLength(self):
  2431.         # NEEDS
  2432.         return 42.42
  2433.  
  2434.     def onViewedExecute(self):
  2435.         if self.item.idExists():
  2436.             self.item.markItemSeen()
  2437.         self.dcOnViewed = None
  2438.  
  2439.     def onViewed(self):
  2440.         if self.dcOnViewed or self.item.getSeen():
  2441.             return
  2442.         self.dcOnViewed = eventloop.addTimeout(5, self.onViewedExecute, "Mark item viewed")
  2443.  
  2444.     def onViewedCancel(self):
  2445.         if self.dcOnViewed:
  2446.             self.dcOnViewed.cancel()
  2447.             self.dcOnViewed = None
  2448.  
  2449.     # Return the ID that is used by a template to indicate this item 
  2450.     def getID(self):
  2451.         return self.item.getID()
  2452.  
  2453.     def __getattr__(self, attr):
  2454.         return getattr(self.item, attr)
  2455.  
  2456. def mappableToPlaylistItem(obj):
  2457.     return (isinstance(obj, item.Item) and obj.isDownloaded())
  2458.  
  2459. def mapToPlaylistItem(obj):
  2460.     return PlaylistItemFromItem(obj)
  2461.  
  2462. def _getThemeHistory():
  2463.     if len(views.themeHistories) > 0:
  2464.         th = views.themeHistories[0]
  2465.         th.checkNewTheme()
  2466.         return th
  2467.     else:
  2468.         return theme.ThemeHistory()
  2469.  
  2470. def _getInitialChannelGuide():
  2471.     default_guide = None
  2472.     newGuide = False
  2473.     for guideObj in views.guides:
  2474.         if guideObj.getDefault():
  2475.             default_guide = guideObj
  2476.             break
  2477.  
  2478.     if default_guide is None:
  2479.         newGuide = True
  2480.         logging.info ("Spawning Miro Guide...")
  2481.         default_guide = guide.ChannelGuide(config.get(prefs.CHANNEL_GUIDE_URL),
  2482.             config.get(prefs.CHANNEL_GUIDE_ALLOWED_URLS).split())
  2483.         initialFeeds = resources.path("initial-feeds.democracy")
  2484.         if os.path.exists(initialFeeds):
  2485.             urls = subscription.parseFile(initialFeeds)
  2486.             if urls is not None:
  2487.                 for url in urls:
  2488.                     feed.Feed(url, initiallyAutoDownloadable=False)
  2489.             dialog = dialogs.MessageBoxDialog(_("Custom Channels"), Template(_("You are running a version of $longAppName with a custom set of channels.")).substitute(longAppName=config.get(prefs.LONG_APP_NAME)))
  2490.             dialog.run()
  2491.             controller.initial_feeds = True
  2492.  
  2493.     return (newGuide, default_guide)
  2494.  
  2495. # Race conditions:
  2496.  
  2497. # We do the migration in the dl_daemon if the dl_daemon knows about it
  2498. # so that we don't get a race condition.
  2499.  
  2500. @eventloop.asUrgent
  2501. def changeMoviesDirectory(newDir, migrate):
  2502.     if not util.directoryWritable(newDir):
  2503.         dialog = dialogs.MessageBoxDialog(_("Error Changing Movies Directory"), 
  2504.                 _("You don't have permission to write to the directory you selected.  Miro will continue to use the old videos directory."))
  2505.         dialog.run()
  2506.         return
  2507.  
  2508.     oldDir = config.get(prefs.MOVIES_DIRECTORY)
  2509.     config.set(prefs.MOVIES_DIRECTORY, newDir)
  2510.     if migrate:
  2511.         views.remoteDownloads.confirmDBThread()
  2512.         for download in views.remoteDownloads:
  2513.             if download.isFinished():
  2514.                 logging.info ("migrating %s", download.getFilename())
  2515.                 download.migrate(newDir)
  2516.         # Pass in case they don't exist or are not empty:
  2517.         try:
  2518.             os.rmdir(os.path.join (oldDir, 'Incomplete Downloads'))
  2519.         except:
  2520.             pass
  2521.         try:
  2522.             os.rmdir(oldDir)
  2523.         except:
  2524.             pass
  2525.     util.getSingletonDDBObject(views.directoryFeed).update()
  2526.  
  2527. @eventloop.asUrgent
  2528. def saveVideo(currentPath, savePath):
  2529.     logging.info("saving video %s to %s" % (currentPath, savePath))
  2530.     try:
  2531.         shutil.copyfile(currentPath, savePath)
  2532.     except:
  2533.         title = _('Error Saving Video')
  2534.         name = os.path.basename(currentPath)
  2535.         text = _('An error occured while trying to save %s.  Please check that the file has not been deleted and try again.') % util.clampText(name, 50)
  2536.         dialogs.MessageBoxDialog(title, text).run()
  2537.         logging.warn("Error saving video: %s" % traceback.format_exc())
  2538.